Merge branch 'main' into MNTOR-2069-README

This commit is contained in:
Raphael Okafor Jr 2023-09-19 07:03:20 -04:00 коммит произвёл GitHub
Родитель 7b104ed86f bb08ba7f28
Коммит 812566b50a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
134 изменённых файлов: 5129 добавлений и 1824 удалений

Просмотреть файл

@ -7,6 +7,18 @@ executors:
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
node_with_python:
docker:
- image: cimg/node:18.12.1
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
- image: cimg/python:3.9.7
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
python:
docker:
- image: cimg/python:3.9.7
@ -43,11 +55,17 @@ orbs:
jobs:
lint:
executor: node
executor: node_with_python
steps:
- checkout
- node/install-packages
- run: sudo apt-get update
- run: sudo apt-get install python3.10-venv
- run: npm run build-glean
- run: npm run lint
- run: cp .env-dist .env
# Runs type checking:
- run: npm run build
deploy:
docker:
- image: docker:stable-git

Просмотреть файл

@ -141,8 +141,14 @@ NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT=190
PREMIUM_PRODUCT_ID=prod_NErZh679W62lai
PREMIUM_PLAN_ID_US=price_1MUNq0Kb9q6OnNsL4BoJgepf
MONTHLY_SUBSCRIBERS_QUOTA=
MONTHLY_SCANS_QUOTA=
STATS_TOKEN=
# Support article link
NEXT_PUBLIC_MONITOR_SUPPORT_URL=https://support.mozilla.org/kb/firefox-monitor
# GCP PubSub Project ID and subscription name
GCP_PUBSUB_PROJECT_ID=
GCP_PUBSUB_TOPIC_NAME=
GCP_PUBSUB_SUBSCRIPTION_NAME=

4
.gitignore поставляемый
Просмотреть файл

@ -63,3 +63,7 @@ next-env.d.ts
# Sentry Auth Token
.sentryclirc
# Glean
.venv
/src/telemetry/generated/

Просмотреть файл

@ -20,3 +20,6 @@ src/middleware/
# These should be ignored anyway
coverage/
playwright-report/
# Ignore Glean generated files
src/telemetry/generated/

Просмотреть файл

@ -4,6 +4,12 @@ RUN addgroup -g 10001 app && \
adduser -D -G app -h /app -u 10001 app
RUN rm -rf /tmp/*
# Install Python
ENV PYTHONUNBUFFERED=1
RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
RUN python3 -m ensurepip
RUN pip3 install --no-cache --upgrade pip setuptools
WORKDIR /app
USER app
@ -18,7 +24,7 @@ RUN npm ci --audit=false && rm -rf ~app/.npm /tmp/*
COPY .env-dist ./.env
ARG S3_BUCKET
ENV S3_BUCKET=$S3_BUCKET
RUN npm run build
RUN GLEAN_PYTHON=python GLEAN_PIP=pip npm run build
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE

Просмотреть файл

@ -80,6 +80,12 @@ We track commits that are largely style/formatting via `.git-blame-ignore-revs`.
pip3 install -r .github/requirements.txt
```
5. Generate required Glean files (needs re-ran anytime Glean `.yaml` files are updated):
```sh
npm run build-glean
```
### Run
1. To run the server similar to production using a build phase, which includes minified and bundled assets:
@ -98,28 +104,35 @@ We track commits that are largely style/formatting via `.git-blame-ignore-revs`.
2. You may receive the error `Required environment variable was not set`. If this is the case, get the required env var(s) from another team member or ask in #fx-monitor-engineering. Otherwise, if the server started successfully, navigate to [localhost:6060](http://localhost:6060/)
### Cloud Functions
### PubSub
User breach notifications from HIBP are handled by a [GCP Cloud Function](https://cloud.google.com/functions). This can be emulated locally:
Monitor uses GCP PubSub for processing incoming breach data, this can be tested locally using an emulator: https://cloud.google.com/pubsub/docs/emulator
#### Run the GCP PubSub emulator:
```sh
npm run functions
gcloud beta emulators pubsub start --project=your-project-name
```
This function serves the `/api/v1/hibp/notify` route, which is responsible for:
1. receiving HTTP POST payloads with a bearer token from HIBP
2. emailing any affected users
This can be tested using curl:
### In a different shell, set the environment to point at the emulator and run Monitor in dev mode:
```sh
curl \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer unsafe-default-token-for-dev' \
-X POST \
-d '{"breachName": "Gravatar", "hashPrefix": "...", "hashSuffixes": ["..."]}' \
localhost:8080/api/v1/hibp/notify
$(gcloud beta emulators pubsub env-init)
npm run dev
```
### Incoming WebHook requests from HIBP will be of the form:
```sh
curl -d '{ "breachName": "000webhost", "hashPrefix": "test", "hashSuffixes": ["test"] }' \
-H "Authorization: Bearer unsafe-default-token-for-dev" \
http://localhost:6060/api/v1/hibp/notify
```
### This pubsub queue will be consumed by this cron job, which is responsible for looking up and emailing impacted users:
```sh
node src/scripts/emailBreachAlerts.js
```
### Database

Просмотреть файл

@ -1,8 +1,8 @@
about:
description: Nimbus Feature Manifest for Monitor Web testing
channels:
- beta
- release
- staging
- production
features:
example-feature:
description: An example feature
@ -16,9 +16,9 @@ features:
type: Option<String>
default: null
defaults:
- channel: beta
- channel: staging
value: { "enabled": true }
- channel: release
- channel: production
value: { "something": "wicked" }
types:

Просмотреть файл

@ -30,7 +30,13 @@ fix-flow-data-broker-profiles-view-data-broker-profiles-view-info-on-sites = Vie
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-trigger-label = More info
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-title = About these profiles
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-paragraph1 = Data brokers make up a $240 billion industry of gathering your personal info and selling it to anyone searching for you. They scrape your data from publicly available sources to create profiles that include your name(s), current and previous addresses, family member names, criminal history, and much more.
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-paragraph2 = We found these profiles by searching 190 data broker sites for the name, location, and date of birth you provided. These profiles are currently live and available to anyone searching for you.
# Variables:
# $data_broker_sites_total_num is the total number of data broker sites available to scan.
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-paragraph2 =
{ $data_broker_sites_total_num ->
[one] We found these profiles by searching { $data_broker_sites_total_num } data broker site for the name, location, and date of birth you provided. These profiles are currently live and available to anyone searching for you. { -brand-mozilla } is not associated with these data broker sites.
*[other] We found these profiles by searching { $data_broker_sites_total_num } data broker sites for the name, location, and date of birth you provided. These profiles are currently live and available to anyone searching for you. { -brand-mozilla } is not associated with these data broker sites.
}
fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-confirm = OK
fix-flow-data-broker-profiles-view-data-broker-profiles-view-profile = View your profile
fix-flow-data-broker-profiles-view-data-broker-profiles-button-view-more = View more

Просмотреть файл

@ -126,12 +126,6 @@ exposure-card-credit-card = Credit Card
exposure-card-password = Password
exposure-card-ip-address = IP Address
exposure-card-other = Other
# Variables:
# $exposure_num is the number of exposures found for a particular type, e.g. 3 found
exposure-card-num-found =
{ $exposure_num ->
*[other] { $exposure_num } found
}
exposure-card-description-info-for-sale-part-one = This site is selling and publishing <data_broker_link>details about you.</data_broker_link>
exposure-card-description-info-for-sale-part-two = Remove this profile to protect your privacy.
# Variables:
@ -149,6 +143,10 @@ exposure-card-label-exposure-type = Exposure type
exposure-card-label-date-found = Date found
# Status of the exposure card, could be In Progress, Fixed or Action Needed
exposure-card-label-status = Status
# Variables:
# $category_label is the data breach exposure type that was leaked. Eg. Email, IP Address.
# $count is the number of times that the data type was leaked.
exposure-card-label-and-count = { $category_label }: { $count }
# About Exposure Types Modal
@ -286,6 +284,20 @@ false-door-test-phase-2-content-part-two-dashboard = Automatically remove data f
premium-upsell-dialog-title = Choose the level of protection thats right for you
# Guided Resolution Flow
guided-resolution-flow-exit = Return to dashboard
guided-resolution-flow-back-arrow = Go to previous step
guided-resolution-flow-next-arrow = Go to next step
# Variables
# $estimated_time is the amount of time it would take for a user to manually resolve a leaked password breach. It will always be a number greater than 1.
# "Est." is shortform for "Estimated".
# "mins" is shortform for "minutes".
data-broker-profiles-estimated-time = Est. time to complete: { $estimated_time } mins
# Variables
# $exposure_reduction is the percentage of exposures that are data brokers.
data-broker-profiles-exposure-reduction = Exposure reduction: { $exposure_reduction }%
# High Risk Data Breaches
high-risk-breach-heading = Heres what to do
@ -331,6 +343,16 @@ high-risk-breach-social-security-description = Scammers can open up new loans or
high-risk-breach-social-security-step-one = Protect yourself by <link_to_info>setting up a fraud alert or freezing your credit.</link_to_info>
high-risk-breach-social-security-step-two = <link_to_info>Check your credit report</link_to_info> for unrecognized accounts.
# Social Security Number Modal
ssn-modal-title = About fraud alerts and credit freezes
ssn-modal-description-fraud-part-one = <b>A fraud alert</b> requires businesses to verify your identity before it issues new credit in your name. Its free, lasts one year, and wont negatively affect your credit score.
ssn-modal-description-fraud-part-two = To set one up, contact any one of the three credit bureaus. You dont have to contact all three.
ssn-modal-description-freeze-credit-part-one = <b>Freezing your credit</b> prevents anyone from opening a new account in your name. Its free and wont negatively affect your credit score, but youll need to unfreeze it before opening any new accounts.
ssn-modal-description-freeze-credit-part-two = To freeze your credit, contact each of the three credit bureaus — <equifax_link>Equifax</equifax_link>, <experian_link>Experian</experian_link>, and <transunion_link>TransUnion</transunion_link>.
ssn-modal-learn-more = Learn more about fraud alerts and credit freezes
ssn-modal-ok = OK
# PIN Breaches
high-risk-breach-pin-title = Your PIN was exposed
@ -351,3 +373,86 @@ high-risk-breach-none-sub-description-bank-account = Bank account info
high-risk-breach-none-sub-description-cc-number = Credit card numbers
high-risk-breach-none-sub-description-pin = PINs
high-risk-breach-none-continue = Continue
# Security recommendations
security-recommendation-steps-label = Security recommendations
security-recommendation-steps-title = Heres our advice:
security-recommendation-steps-cta-label = Got it!
# Phone security recommendation
security-recommendation-phone-title = Protect your phone number
# $num_breaches is the number of breaches where the phone number was found.
security-recommendation-phone-summary = { $num_breaches ->
[one] Your phone number was exposed in { $num_breaches } data breach:
*[other] Your phone number was exposed in { $num_breaches } data breaches:
}
security-recommendation-phone-description = Unfortunately you cant take it back. But there are steps you can take to make sure you stay safe.
security-recommendation-phone-step-one = Block spam numbers to prevent more junk calls
security-recommendation-phone-step-two = Dont click on links in texts from unknown senders; if it appears to be from a trusted source, call directly to confirm
security-recommendation-phone-step-three = Use a <link_to_info>{ -brand-relay } phone mask</link_to_info> to protect your phone in the future
# Email security recommendation
security-recommendation-email-title = Protect your email address
# $num_breaches is the number of breaches where the email address was found.
security-recommendation-email-summary = { $num_breaches ->
[one] Your email address was exposed in { $num_breaches } data breach:
*[other] Your email address was exposed in { $num_breaches } data breaches:
}
security-recommendation-email-description = Unfortunately you cant fix this. But there are steps you can take to protect yourself.
security-recommendation-email-step-one = Dont click on links in emails from unknown senders; If it appears to be from trusted source, call directly to confirm
security-recommendation-email-step-two = Be aware of <link_to_info>phishing scams</link_to_info>
security-recommendation-email-step-three = Mark suspicious emails as spam and block the sender
security-recommendation-email-step-four = Use <link_to_info>{ -brand-relay } email masks</link_to_info> to protect your email in the future
# IP security recommendation
security-recommendation-ip-title = Use a VPN for added privacy
# $num_breaches is the number of breaches where the IP address was found.
security-recommendation-ip-summary = { $num_breaches ->
[one] Your IP address was exposed in { $num_breaches } data breach:
*[other] Your IP address was exposed in { $num_breaches } data breaches:
}
security-recommendation-ip-description = Your IP address pinpoints your location and internet service provider. Hackers could use this information to find your location or try to connect to your devices.
security-recommendation-ip-step-one = Use a VPN (such as <link_to_info>{ -brand-mozilla-vpn }</link_to_info>) to hide your real IP address and use the internet privately.
# Leaked Passwords
# Variables
# $breach_name is the name of the breach where the leaked password was found.
leaked-passwords-title = Your { $breach_name } password was exposed.
# Variables
# $breach_date is the date when the breach occurred.
leaked-passwords-summary = It appeared in a data breach on { $breach_date }.
leaked-passwords-description = Scammers can access your account and will likely try to use it on other accounts to see if youve used the same password. Change it anywhere youve used it to protect yourself.
leaked-passwords-steps-title = Heres what to do
leaked-passwords-steps-subtitle = This requires access to your account, so youll need to manually fix it.
# Variables
# $breach_name is the name of the breach where the leaked password was found.
leaked-passwords-step-one = Change your password on <link_to_breach_site>{ $breach_name }</link_to_breach_site>.
leaked-passwords-step-two = Change it anywhere else youve used it.
leaked-passwords-mark-as-fixed = Mark as fixed
leaked-passwords-skip = Skip for now
# Variables
# $estimated_time is the amount of time it would take for a user to manually resolve a leaked password breach. It will always be a number greater than 1.
# "Est." is shortform for "Estimated".
# "mins" is shortform for "minutes".
leaked-passwords-estimated-time = Est. time to complete: { $estimated_time } mins per site
# Leaked Security Questions
leaked-security-questions-title = Your security questions were exposed
# Variables
# $breach_name is the name of the breach where the leaked security questions were found.
# $breach_date is the date when the breach occurred.
# An example of this string is Twitter on 13/09/18.
leaked-security-questions-summary = They appeared in a data breach on { $breach_name } on { $breach_date }.
leaked-security-questions-description = Scammers can use these to access your accounts, and any other site where youve used the same security questions. Update them now to protect your accounts.
leaked-security-questions-steps-title = Heres what to do
leaked-security-questions-steps-subtitle = This requires access to your account, so youll need to manually fix it.
# Variables
# $breach_name is the name of the breach where the security questions were found.
leaked-security-questions-step-one = Update your security questions on <link_to_breach_site>{ $breach_name }</link_to_breach_site>.
leaked-security-questions-step-two = Update them on any other site where you used the same security questions. Be sure to use different security questions for every account.

Просмотреть файл

@ -780,7 +780,7 @@ brand-mozilla-vpn = { -brand-mozilla-vpn }
menu-button-title = Μενού χρήστη
menu-button-alt = Άνοιγμα μενού χρήστη
menu-list-accessible-label = Μενού λογαριασμού
menu-item-fxa = Διαχείριση του { -brand-fx-account(case: "gen", capitalization: "lower") } σας
menu-item-fxa = Διαχείριση { -brand-fx-account(case: "gen", capitalization: "lower") }
menu-item-fxa-alt = Άνοιγμα σελίδας { -brand-fx-account(case: "gen", capitalization: "lower") }
menu-item-settings = Ρυθμίσεις
menu-item-settings-alt = Άνοιγμα σελίδας ρυθμίσεων

Просмотреть файл

@ -5,5 +5,15 @@
### Dialog window that allows a user to add a new email address to be monitored
add-email-add-another-heading = Add another email address
close-dialog-alt = Close dialog
# $total is the number of emails a user is allowed to add
add-email-your-account-includes =
{ $total ->
[one] Your account includes monitoring of { $total } email address. Add a new email address to see if its been involved in a breach.
*[other] Your account includes monitoring of up to { $total } email addresses. Add a new email address to see if its been involved in a breach.
}
add-email-address-input-label = Email Address
add-email-send-verification-button = Send verification link
# $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>`
add-email-verify-the-link = Verify the link sent to { $email } to add it to { -brand-fx-monitor }. Manage all email addresses in <a { $settings-href }>Settings</a>.

Просмотреть файл

@ -708,6 +708,8 @@ ad-unit-6-before-you-complete = Before you complete that next signup, use an ema
-brand-firefox = Firefox
-brand-fx-monitor = Firefox Monitor
-brand-mozilla = Mozilla
-brand-premium = Premium
-brand-monitor-premium = Monitor Premium
-brand-mozilla-foundation = Mozilla Foundation
-brand-github = GitHub
-brand-mozilla-vpn = Mozilla VPN
@ -721,6 +723,7 @@ ad-unit-6-before-you-complete = Before you complete that next signup, use an ema
## Search Engine Optimization
meta-desc-2 = Find out if youve been part of a data breach with { -brand-fx-monitor }. Well help you understand what to do next and continuously monitor for any new breaches.
## Header
@ -732,6 +735,10 @@ sign-in = Sign In
site-nav-breaches-link = Resolve Data Breaches
site-nav-settings-link = Settings
site-nav-help-link = Help and Support
# This call-out is above 2 image links for Firefox Relay and Mozilla VPN
site-nav-ad-callout = Try our other security tools:
brand-relay = { -brand-relay }
brand-mozilla-vpn = { -brand-mozilla-vpn }
## User menu
@ -739,8 +746,11 @@ menu-button-title = User menu
menu-button-alt = Open user menu
menu-list-accessible-label = Account menu
menu-item-fxa = Manage your { -brand-fx-account }
menu-item-fxa-alt = Open { -brand-fx-account } page
menu-item-settings = Settings
menu-item-settings-alt = Open settings page
menu-item-help = Help and support
menu-item-help-alt = Open help and support page
menu-item-logout = Sign out
## Footer
@ -748,12 +758,14 @@ menu-item-logout = Sign out
mozilla = { -brand-Mozilla }
terms-and-privacy = Terms and Privacy
github = { -brand-github }
footer-nav-all-breaches = All Breaches
## Error page
# Variables:
# $errorCode (number) - "404"
error-page-error-404-title = { $errorCode } Page not found
error-page-error-404-copy = Were sorry, the page youre looking for no longer exists.
error-page-error-404-cta-button = Go back
# Variables:
# $errorCode (number) - the status code of the error, e.g. 403
@ -762,7 +774,20 @@ error-page-error-other-copy = Please try again or come back later
## Breach overview page
all-breaches-headline-2 = All breaches detected by { -brand-fx-monitor }
all-breaches-lead = We monitor all known data breaches to find out if your personal information was compromised. Heres a complete list of all of the breaches that have been reported since 2007.
search-breaches = Search Breaches
# the kind of user data exposed to hackers in data breach.
exposed-data = Exposed data:
## Public breach detail page
find-out-if-2 = Find out if you were involved in this breach
find-out-if-description = Well help you quickly see if your email address was exposed in this breach, and understand what to do next.
breach-detail-cta-signup = Check for breaches
## Floating banner
floating-banner-text = Boost your online security with news, tips, and updates from { -brand-Mozilla }.
floating-banner-link-label = Sign up
floating-banner-dismiss-button-label = No thanks

Просмотреть файл

@ -2,6 +2,20 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
breach-meta-title = { -brand-fx-monitor } - Dashboard
breach-all-meta-title = { -brand-fx-monitor } - All Data Breaches
breach-all-meta-social-title = All Breaches Detected by { -brand-fx-monitor }
breach-all-meta-social-description = Browse the complete list of known breaches detected by { -brand-fx-monitor }, then find out if your information was exposed.
# Variables:
# $company (String) - Name of the company that was breached, e.g. "PHP Freaks"
breach-detail-meta-title = { -brand-fx-monitor } - { $company } Data Breach
# Variables:
# $company (String) - Name of the company that was breached, e.g. "PHP Freaks"
breach-detail-meta-social-title = Were you affected by the { $company } Data Breach?
breach-detail-meta-social-description = Use { -brand-fx-monitor } to find out if your personal information was exposed in this breach, and understand what to do next.
breach-scan-meta-title = { -brand-fx-monitor } - Breach Results
breach-scan-meta-social-title = { -brand-fx-monitor } Breach Results
breach-scan-meta-social-description = Sign in to { -brand-fx-monitor } to resolve breaches and get continuous monitoring for any new known breaches.
## Breaches header
@ -9,6 +23,12 @@
breach-chart-title = Breached data
# $email-select is an interactive <select> element displaying the current email address
breach-heading-email = Data breaches for { $email-select }
# $count is the number of emails a user has added out of $total allowed
emails-monitored =
{ $total ->
[one] { $count } of { $total } email monitored
*[other] { $count } of { $total } emails monitored
}
# link to Settings page where user can add/remove emails and set message preferences
manage-emails-link = Manage emails
@ -26,51 +46,115 @@ column-detected = DETECTED
column-status-badge-resolved = Resolved
# “Active” is shown next to a breach if the user still has at least one recommended action to perform in response to the breach.
column-status-badge-active = Active
breaches-resolve-heading = Resolve this breach:
breaches-none-headline = No breaches found
# Variables:
# $email (String) - An email address that we did not find breaches for, e.g. `someone@example.com`
breaches-none-copy = Good news! No known breaches were reported for { $email }. Well keep monitoring this email and will let you know if any new breaches occur.
breaches-none-cta-blurb = Would you like to monitor another email?
breaches-none-cta-button = Add email address
breaches-all-resolved-headline = All breaches resolved
# Variables:
# $email (String) - An email address for which all breaches have been resolved, e.g. `someone@example.com`
breaches-all-resolved-copy = Nicely done! Youve resolved all breaches for { $email }. Well keep monitoring this email and will let you know if any new breaches occur.
breaches-all-resolved-cta-blurb = Would you like to monitor another email?
breaches-all-resolved-cta-button = Add email address
# $breachDate and $addedDate are dates that should be localized via JS DateTimeFormat(). $dataClasses is a list of strings from data-classes.ftl that should be localized via JS ListFormat()
# Variables:
# $breachDate (String) - Date of the breach
# $companyName (String) - Name of the company where the breach occurred
breach-description = On { $breachDate }, { $companyName } was breached. Once the breach was discovered and verified, it was added to our database on { $addedDate }. This breach included: { $dataClasses }
## Links that we might refer to when prompting the user to make changes after a breach
breach-checklist-link-firefox-relay = { -brand-relay }
breach-checklist-link-password-manager = { -brand-firefox } Password Manager
breach-checklist-link-mozilla-vpn = { -brand-mozilla-vpn }
## Prompts the user for changes when there is a breach detected of password
breach-checklist-pw-header-text = Update your passwords and enable two-factor authentication (2FA).
# The `breached-company-link` tags will be replaced with link tags or stripped if no link is available.
# Variables:
# $passwordManagerLink (string) - a link to the password manager documentation, with { -breach-checklist-link-password-manager } as the label
breach-checklist-pw-body-text = In most cases, wed recommend that you change your password on the companys website. But <b>their website may be down or contain malicious content</b>, so use caution if you <breached-company-link>visit the site</breached-company-link>. For added protection, make sure youre using unique passwords for all accounts, so that any leaked passwords cant be used to access other accounts. { $passwordManagerLink } can help you securely keep track of all of your passwords.
## Prompts the user for changes when there is a breach detected of email
# Variables:
# $firefoxRelayLink (string) - a link to Firefox Relay, with { -breach-checklist-link-firefox-relay } as the label
breach-checklist-email-header-2 = Protect your email with an email masking service like { $firefoxRelayLink }.
breach-checklist-email-body = This can hide your true email address while forwarding emails to your real inbox.
## Prompts the user for changes when there is a breach detected of social security number
# Credit reports list your bill payment history, loans, current debt, and other financial information.
# They show where you work and live and whether you've been sued, arrested, or filed for bankruptcy.
breach-checklist-ssn-header = Monitor your credit report for accounts, loans, or credit cards you dont recognize.
# A security freeze prevents prospective creditors from accessing your credit file.
# Creditors typically won't offer you credit if they can't access your credit reporting file,
# so a security freeze, also called a credit freeze, prevents you or others from opening accounts in your name.
# This will only be shown to users in the US.
# Variables:
# $equifaxLink (string) - a link to the Equifax website
# $experianLink (string) - a link to the Experian website
# $transUnionLink (string) - a link to the TransUnion website
breach-checklist-ssn-body-2 = You can also consider freezing your credit on { $equifaxLink }, { $experianLink } and { $transUnionLink } to stop scammers from opening new accounts in your name. Its free and wont affect your credit score.
## Prompts the user for changes when there is a breach detected of credit card
breach-checklist-cc-header = Report this breach to your credit card issuer and request a new card with a new number.
breach-checklist-cc-body = You should also review your credit card statements for unrecognized charges.
## Prompts the user for changes when there is a breach detected of bank account
breach-checklist-bank-header = Notify your bank immediately that your account number has been compromised.
breach-checklist-bank-body = Doing so faster could give you more legal protections to help you recover any losses. Youll also want to check your accounts for any unrecognized charges.
## Prompts the user for changes when there is a breach detected of pin
breach-checklist-pin-header = Notify your card issuer and change your PIN immediately.
breach-checklist-pin-body = Make sure your new PIN, or any other PIN, doesnt include easily guessed numbers such as your birth date or address.
## Prompts the user for changes when there is a breach detected of IP address
# Variables:
# $mozillaVpnLink (string) - a link to the Mozilla VPN website, with { -breach-checklist-link-mozilla-vpn } as the label
breach-checklist-ip-header-2 = Use the internet privately with a VPN, such as { $mozillaVpnLink }.
breach-checklist-ip-body = Your IP address (Internet Protocol address) pinpoints your location and internet service provider. A VPN can hide your real IP address so you can use the internet privately.
## Prompts the user for changes when there is a breach detected of physical address
breach-checklist-address-header = Change any passwords or PINs that include any part of your address.
breach-checklist-address-body = Addresses are easy to find in public records and can make those passwords and PINs easy to guess.
## Prompts the user for changes when there is a breach detected of date of birth
breach-checklist-dob-header = Change any passwords or PINs that include your date of birth.
breach-checklist-dob-body = Birth dates are easy to find in public records, and people who find it could easily guess your PIN.
## Prompts the user for changes when there is a breach detected of phone number
# Variables:
# $firefoxRelayLink (string) - a link to Firefox Relay, with { -breach-checklist-link-firefox-relay } as the label
breach-checklist-phone-header-2 = Protect your phone number with a masking service like { $firefoxRelayLink }, which hides your true phone number.
## Prompts the user for changes when there is a breach detected of security questions
breach-checklist-sq-header-text = Update your security questions.
# The `breached-company-link` tags will be replaced with link tags or stripped if no link is available.
breach-checklist-sq-body-text = In most cases, wed recommend that you update your security questions on the companys website. But <b>their website may be down or contain malicious content</b>, so use caution if you <breached-company-link>visit the site</breached-company-link>. For added protection, update these security questions on any important accounts where youve used them, and create unique passwords for all accounts.
## Prompts the user for changes when there is a breach detected of historical password
breach-checklist-hp-header = Create unique, strong passwords for any account where youve re-used passwords.
# Variables:
# $passwordManagerLink (string) - a link to the password manager documentation, with { -breach-checklist-link-password-manager } as the label
breach-checklist-hp-body-2 = A password manager like { $passwordManagerLink } (which is free and built-in to the { -brand-firefox } browser) can help you keep track of all your passwords and access them securely from all your devices.
## Prompts the user for changes when there is a breach detected of other types
# Variables:
# $breachDate (String) - Date of the breach
# $companyName (String) - Name of the company where the breach occurred
breach-checklist-general-header = Reach out to { $companyName } to inform them about this breach and ask for specific steps you can take.

Просмотреть файл

@ -25,6 +25,7 @@ career-levels = Career levels
cellular-network-names = Cellular network names
charitable-donations = Charitable donations
chat-logs = Chat logs
clothing-sizes = Clothing sizes
credit-card-cvv = Credit card CVV
credit-cards = Credit cards
credit-status-information = Credit status information
@ -77,6 +78,8 @@ job-titles = Job titles
# This string refers to vehicle license plates.
licence-plates = Licence Plates
living-costs = Living costs
# This string refers to financial loans.
loan-information = Loan information
login-histories = Login histories
mac-addresses = MAC addresses
marital-statuses = Marital statuses

Просмотреть файл

@ -0,0 +1,28 @@
exposure-landing-hero-heading = Find out if your personal information has been compromised
exposure-landing-hero-lead = Stay safe with privacy tools from the makers of { -brand-firefox } that protect you from hackers and companies that publish and sell your personal information. Well alert you of any known data breaches, find and remove your exposed info and continually watch for new exposures.
exposure-landing-hero-email-label = Email Address
exposure-landing-hero-email-placeholder = Enter email address
exposure-landing-hero-cta-label = Check for breaches
exposure-landing-result-loading = Loading, please wait…
exposure-landing-result-error = Something went wrong while checking for breaches. Please refresh the page and try again.
# Variables:
# $email (string) - The user's email address, used to identify their data in breaches
# $count (number) - Number of data breaches in which the user's data was found
exposure-landing-result-hero-heading =
{ $count ->
[one] We found <email>{ $email }</email> exposed in <count>1</count> data breach.
*[other] We found <email>{ $email }</email> exposed in <count>{ $count }</count> data breaches.
}
exposure-landing-result-card-added = Breach Added:
exposure-landing-result-card-data = Exposed Data:
exposure-landing-result-card-nothing = No breaches found
exposure-landing-result-footer-attribution = Breach data provided by <hibp-link>{ -brand-HIBP }</hibp-link>
exposure-landing-result-overflow-hero-lead = Sign in to get clear steps on how to resolve these breaches, view all breaches, and get continuous monitoring for any new known breaches.
exposure-landing-result-overflow-hero-cta-label = Sign in to resolve breaches
exposure-landing-result-overflow-footer-cta-label = Sign in to view all
exposure-landing-result-some-hero-lead = Sign in to get clear steps on how to resolve these breaches, view all breaches, and get continuous monitoring for any new known breaches.
exposure-landing-result-some-hero-cta-label = Sign in to resolve breaches
exposure-landing-result-some-footer-cta-label = Sign in to resolve breaches
exposure-landing-result-none-hero-lead = Good news! No known breaches were found. Stay safe by signing up for alerts for new breaches. Well keep monitoring this email and let you know if it appears in a new breach.
exposure-landing-result-none-hero-cta-label = Get alerts about new breaches
exposure-landing-result-none-footer-cta-label = Sign up for alerts

Просмотреть файл

@ -3,12 +3,12 @@
## Hero
find-out-if-breached = Find out if youve been in a data breach
get-started = Get started
## Why use Firefox Monitor?
why-use-monitor = Why use { -brand-fx-monitor }?
identifying-breaches = Identifying and resolving data breaches is an important step in ensuring your online privacy.
protect-account = Protect your accounts
protect-account-prevent-hackers = Prevent hackers from getting into your accounts with breached passwords.
prevent-fraud = Prevent fraud and cybercrimes
@ -18,13 +18,19 @@ get-alerts-find-out = Find out whenever your personal information is compromised
## Heres how it works
how-it-works = Heres how it works
check-for-breaches = Check for breaches
check-for-breaches-we-search = Well search all known data breaches since 2007 to see if your info was compromised.
protect-accounts = Protect your accounts
protect-accounts-clear-steps = Well give you clear steps on what to do next for any data breach youve been involved in.
alerts-for-breaches = Get alerts for new breaches
alerts-for-breaches-monitor-new = Well continually monitor for new data breaches and let you know if youve been affected.
## Your privacy is safe with us
safe-with-us = Your privacy is safe with us
parent-company = At { -brand-Mozilla }, the parent company of { -brand-firefox } and { -brand-fx-monitor }, we believe individuals security and privacy on the internet is fundamental and must not be treated as optional.
our-mission = Our mission is to build a better internet — one where people can shape their own experiences, feel empowered and stay safe. Were wholly owned by the { -brand-mozilla-foundation }, a non-profit, which enables us to put individual and public benefit over profit.
learn-more-mission = Learn more about our mission
## Top questions about Firefox Monitor
@ -36,7 +42,12 @@ what-is-breach = What exactly is a data breach?
when-info-exposed = A data breach happens when personal or private information gets exposed, stolen or copied without permission. These security incidents can be a result of cyber attacks to websites, apps or any database where peoples personal information resides. A data breach can also happen by accident like if someones login credentials accidentally get posted publicly.
# question and answer
what-do-i-do = I just found out Im in a data breach. What do I do?
visit-monitor-to-learn = Visit { -brand-fx-monitor } to learn what to do after a data breach. Hackers rely on people reusing passwords, so its important to create strong, unique passwords for all your accounts. Keep your passwords in a safe place that only you have access to; this could be the same place where you store important documents or a password manager.
# question and answer
what-gets-exposed = What information gets exposed in data breaches?
depends-on-hackers = Not all breaches expose all the same info. It just depends on what hackers can access. Many data breaches expose email addresses and passwords. Others expose more sensitive information such as credit card numbers, passport numbers and social security numbers.
## See if youve been in a data breach
see-if-data-breach = See if youve been in a data breach
hibp-footer-attribution = Breach data provided by <b>{ -brand-HIBP }</b>

Просмотреть файл

@ -10,10 +10,10 @@ rec-ssn =
rec-pw-1-subhead = Change your password
# Link title
rec-pw-1-cta = Change password for this site
rec-pw-1 =
Make this password unique and different from any others you use.
A good strategy to follow is to combine two or more unrelated
words to create an entire passphrase.
rec-pw-1-2 =
Make this password unique and different from any others you use.
A good strategy to follow is to combine two or more unrelated
words to create an entire passphrase, and include numbers and symbols.
# Recommendation subhead
rec-pw-2-subhead = Update other logins using the same password
# Link title
@ -54,22 +54,13 @@ rec-email-mask-subhead = Use an email mask
rec-email-cta = Try { -brand-relay }
rec-email = Giving out your real email address makes it easier for hackers or trackers to find your passwords or target you online. A service like { -brand-relay } hides your real email address while forwarding emails to your real inbox.
# Recommendation subhead
rec-ip-subhead = Use a service that masks your IP address
# Link title
rec-ip-us-cta = Try { -brand-fpn }
rec-ip-us =
Your Internet Protocol address (IP address) pinpoints your location
and internet service provider. A service like { -brand-fpn }
masks your IP address to hide your location.
rec-ip-non-us =
Your Internet Protocol address (IP address) pinpoints your location
and internet service provider. With a virtual private network (VPN), you can hide your location
and mask your IP address.
rec-ip-subhead-2 = Use a VPN to mask your IP address
# Recommendation subhead
rec-moz-vpn-cta = Try { -brand-mozilla-vpn }
rec-moz-vpn-update =
Your Internet Protocol address (IP address) can reveal your location and internet service provider. A service
like { -brand-mozilla-vpn } hides your IP address and location for your entire device.
rec-moz-vpn-update-2 =
Your Internet Protocol address (IP address) pinpoints your location
and internet service provider. A service like { -brand-mozilla-vpn }
masks your IP address to hide your location.
rec-hist-pw-subhead = Avoid reusing passwords
# Link title
rec-hist-pw-cta-fx = View logins in { -brand-name }

Просмотреть файл

@ -5,6 +5,7 @@
# Settings page
settings-meta-title = { -brand-fx-monitor } - Settings
settings-page-title = { -product-short-name } Settings
## Breach alert preferences
@ -19,9 +20,32 @@ settings-alert-preferences-option-two = Send all breach alerts to the primary em
# $email (string) - Email address
settings-email-label-primary = { $email } (primary)
settings-email-list-title = Monitored email addresses
# Variables:
# $limit (number) - Number of email addresses included in the plan
settings-email-limit-info =
{ $limit ->
[one] Your account includes monitoring of up to { $limit } email.
*[other] Your account includes monitoring of up to { $limit } emails.
}
settings-email-verification-callout = Email verification required
settings-resend-email-verification-link = Resend verification email
settings-add-email-button = Add email address
settings-delete-email-button = Delete email address
# This string is shown beneath each of the users email addresses to indicate
# how many known breaches that email address was found in.
# Variables:
# $breachCount (numer) - Number of breaches
settings-email-number-of-breaches-info =
{ $breachCount ->
[one] Appears in { $breachCount } known breach.
*[other] Appears in { $breachCount } known breaches.
}
## Cancel Premium subscription
settings-cancel-premium-subscription-title = Cancel { -brand-premium } subscription
settings-cancel-premium-subscription-info = Your subscription will revert to a free account after the current billing cycle ends. Your privacy protection scan results will be permanently deleted, and youll only have data breach monitoring for 1 email address.
settings-cancel-premium-subscription-link-label = Cancel from your { -brand-fx-account }
## Deactivate account
@ -35,3 +59,13 @@ settings-email-dialog-title = Add another email address
settings-add-email-text = Add a new email address to see if its been involved in a breach.
settings-email-input-label = Email address
settings-send-email-verification-button = Send verification link
## Unsubscribe Dialog Survey
settings-unsubscribe-dialog-title = Were sorry to see you go. <br /> Will you tell us why youre leaving?
settings-unsubscribe-dialog-info = Your experience is important to us. We read every response and take it into consideration.
settings-unsubscribe-dialog-message-placeholder = What could have gone better?
# $faq_href is the URL for the faq page. HTML tags should not be translated, e.g. `<a>`
settings-unsubscribe-dialog-confirmation = Please note, all of your { -brand-monitor-premium } services will be <a { $faq_href }>permanently deleted</a> after your current billing cycle ends.
settings-unsubscribe-dialog-continue = Continue to cancellation
settings-unsubscribe-dialog-cancel = Never mind, take me back

Просмотреть файл

@ -80,7 +80,7 @@ manage-fxa = Eñangareko { -brand-fxa }
have-an-account = ¿Erekóma mbaete?
fxa-pwt-summary-2 =
Ñeẽñemi mbykýva peteĩ ñeẽnteva ndahasýi ijeikuaa mbaevai apohápe g̃uarã.
Eipuru saivéramo mokõi ñeẽ ha embojopyru tai, papapy ha ambueve.
Eiporu saivéramo mokõi ñeẽ ha embojopyru tai, papapy ha ambueve.
fxa-pwt-summary-4 =
Ñeẽñemi ñangarekoha 1Password, LastPass, Dashlane ha Bitwarden oñongatu ne
ñeẽñemi ha omoinge ñanduti rendápe. Ikatu avei nepytyvõ emoheñói hag̃ua ñeẽñemi hekorosãva.
@ -91,14 +91,14 @@ fxa-what-to-do-blurb-1 =
Eñepyrũkuaárõ tembiapo, eñeẽ ñanduti renda ndive eporandu hag̃ua mbaéichapa embohekopyahúta.
¿Ehecha peteĩ mbaete eikuaaỹva? Ne mbaekuaarã ikatu oñevendémarae
térã oñemyasãima. Kóva avei ikatu hae mbaete nderesaráiva emoheñói hag̃ua térã peteĩ mbaapohaguasu omoambuéva héra.
fxa-what-to-do-subhead-2 = Anive eipuru ñeẽñemi ojekuaareíva ha emoambue opaite tenda eipuruhápe.
fxa-wtd-blurb-2 = Umi mbaevaiapoha tembiporupyahugua oipurujeykuaa ñeẽñemi ojehechakuaáva eike hag̃ua ambue mbaetépe. Emoheñói ñeẽñemi pyahu ha ha haeñóva peteĩteĩva mbaetépe, ejapokuaáva ne mbaete banco pegua, ne ñanduti veve ha ambue ñanduti renda eñongatu hague ne maranduete.
fxa-what-to-do-subhead-2 = Anive eiporu ñeẽñemi ojekuaareíva ha emoambue opaite tenda eiporuhápe.
fxa-wtd-blurb-2 = Umi mbaevaiapoha tembiporupyahugua oiporujeykuaa ñeẽñemi ojehechakuaáva eike hag̃ua ambue mbaetépe. Emoheñói ñeẽñemi pyahu ha ha haeñóva peteĩteĩva mbaetépe, ejapokuaáva ne mbaete banco pegua, ne ñanduti veve ha ambue ñanduti renda eñongatu hague ne maranduete.
fxa-what-to-do-blurb-3 =
Heta mbaevai ohechauka ñanduti veve ha ñeẽñemi año, hákatu oĩ avei omoinge marandu virugua ñemiguáva.
Ne mbaete banco térã kuatiaatã ñemurã papapy ojehechakuaa kuri, emombeu ne báncope ikatuha oiko mbaevai.
Ehechajey umi jeepyre eikuaaỹva.
fxa-what-to-do-subhead-4 = Eñepytyvõuka nemandua hag̃ua opaite ne ñeẽñemíre ha ereko tekorosãme.
fxa-what-to-do-blurb-4 = Umi ñeẽñemi ñangarekoha 1Password, LastPass, Dashlane ha Bitwarden ombyaty ne ñeẽñemi tekorosãme ha oike ñanduti rendápe. Oipuru peteĩ ñeẽñemi ñangarekoha ne pumbyry ha mohendahápe ani hag̃ua ne manduapánte.
fxa-what-to-do-blurb-4 = Umi ñeẽñemi ñangarekoha 1Password, LastPass, Dashlane ha Bitwarden ombyaty ne ñeẽñemi tekorosãme ha oike ñanduti rendápe. Oiporu peteĩ ñeẽñemi ñangarekoha ne pumbyry ha mohendahápe ani hag̃ua ne manduapánte.
# Alerts is a noun
sign-up-for-alerts = Eñemboheraguapy og̃uahẽ hag̃ua kyhyjerã
# Link title
@ -173,11 +173,11 @@ what-is-data-agg-blurb =
Umi mbaekuaarã mbyatyha omonoõ marandu opavave mbaévagui
ha avei ojogua ambue mbaapohaguasúgui. Ombojuaju marandu ovende hag̃ua ambue
mbaapohaguasúpe jekuaaukarãve. Umi ohupytýva koã ñembyai ndorekói
arakaeve viru ñemonda, hákatu umi hekovaíva ñadutípe ikatu oipuru pe marandu omyendague hag̃ua chupe.
arakaeve viru ñemonda, hákatu umi hekovaíva ñadutípe ikatu oiporu pe marandu omyendague hag̃ua chupe.
protect-your-privacy = Emoã ñemigua ñandutípe
no-pw-to-change = Ñanduti renda juavygua ñembyai, ndaipóri ñeẽñemi emoambuevaerã.
avoid-personal-info = Ani eipuru marandu jehegua ñeẽñemíme
avoid-personal-info-blurb = Ndahasýi aramboty jejuhu, kundaharape ha pehengue réra ñandutípe. Ani eipuru koã ñeẽ ne ñeẽñemíme.
avoid-personal-info = Ani eiporu marandu jehegua ñeẽñemíme
avoid-personal-info-blurb = Ndahasýi aramboty jejuhu, kundaharape ha pehengue réra ñandutípe. Ani eiporu koã ñeẽ ne ñeẽñemíme.
## What to do after data breach tips
@ -187,7 +187,7 @@ even-for-old = Avei mbaete itujáva, embohekopyahúke ne ñeẽñemi.
make-new-pw-unique = Ejapo ñeẽñemi pyahúgui iñambue ha haeñóva
strength-of-your-pw = Ne ñeẽñemi mbaretekue oñomoirũ ne rekorosã ñandutí pegua ndive.
create-strong-passwords = Mbaéicha emoheñóita ñeẽñemi hekorosãva
stop-reusing-pw = Anive eipuru peteĩ ñeẽñemi memete
stop-reusing-pw = Anive eiporu peteĩ ñeẽñemi memete
create-unique-pw = Emoheñói ñeẽñemi haetéva ha eñongatu tenda hekorosãvape, peteĩ ñeẽñemi ñangarekohápe.
five-myths = 5 mombeuguau ñeẽñemi ñangarekoha rehegua
create-a-fxa = Emoheñói { -brand-fxa } eñemomarandu hag̃ua ñembyaíre ha og̃uahẽta ndéve kyhyjerã.
@ -252,14 +252,14 @@ ba-next-step-blurb-1 =
Ñeẽñemi hekorosãva ombojopyru taii ha taiguasu,
papapy ha ambueve. Ndoguerekói marandu mbaeteéva aichagua
kundaharape, arareñói térã ára pehengue rehegua.
ba-next-step-2 = Anive eipuru ñeẽñemi ojekuaareíva.
ba-next-step-2 = Anive eiporu ñeẽñemi ojekuaareíva.
ba-next-step-blurb-2 =
Umi hekovaíva ñadutípe ikatu ojuhu ne ñeẽñemi ñanduti ypytũme
ha oipuru ñepyrũ hag̃ua tembiapo ambue ne mbaetépe. Emoãporãve hag̃ua
ha oiporu ñepyrũ hag̃ua tembiapo ambue ne mbaetépe. Emoãporãve hag̃ua
eipurúke ñeẽñemi haetéva peteĩteĩvape g̃uarã.
ba-next-step-3 = Eñepytyvõuka emoheñói hag̃ua ñeẽñemi oikóva ha ereko tekorosãme.
ba-next-step-blurb-3 =
Oipuru ñeẽñemi ñangarekoha omoheñói hag̃ua ñeẽñemi hekorosãva ha haeñóva. Koã ñangarekoha ombyaty opaite ne
Oiporu ñeẽñemi ñangarekoha omoheñói hag̃ua ñeẽñemi hekorosãva ha haeñóva. Koã ñangarekoha ombyaty opaite ne
rembiapo ñepyrũ hekorosãva eike hag̃ua opaite ne mbaeoka guive.
faq1 = Ndaikuaái ko mbaapohaguasu térã ñanduti renda. ¿Mbaére aime ko ñembyaípe?
faq2 = ¿Mbaére ehaarõite ko ñembyai ñemomarandu?
@ -428,7 +428,7 @@ filter-by = Embogua jehechaukaha rupi:
menu = Poravorã
to-affected-email = Emondo ñembyai kyhyjerã ñanduti veve imarãkuapavape
# This string appears in a banner at the top of each page and is followed by a "Learn More" link.
join-firefox = Oĩ peteĩ hendáicha emoã hag̃ua ne ñemigua. Eipuru { -brand-name }.
join-firefox = Oĩ peteĩ hendáicha emoã hag̃ua ne ñemigua. Eiporu { -brand-name }.
# Link title
learn-more-link = Kuaave.
email-sent = ¡Ñandutiveve mondopyre!
@ -475,7 +475,7 @@ how-hackers-work-desc = Emoã ne ñeẽñemi mbaevai apoha ñandutiguá
what-to-do-after-breach-desc = Ejoko ne mbaete eguereko hag̃ua ne marandu mombyry iñañávagui.
create-strong-passwords-desc = Ejapo ne ñeẽñemi imbaretéva, hekorosã ha hasýva ojekuaa hag̃ua.
steps-to-protect-desc = Eikumby kyhyjerã hetavéva eikuaa hag̃ua mbaetépa pe ehekavaerã.
five-myths-desc = Eikuaa mbaéichapa emboykéta ñeẽñemi jepuruvai nombohasýiva hekovaíva rembiapo.
five-myths-desc = Eikuaa mbaéichapa emboykéta ñeẽñemi jeporuvai nombohasýiva hekovaíva rembiapo.
take-further-steps-desc = Ehecha mbaéichapa emboykéta teratee ñemonda ani hag̃ua ñe apañuãi viru rekópe.
# This message appears after a user has successfully updated their communication settings.
changes-saved = ¡Moambuepy ñongatupyre!
@ -667,23 +667,23 @@ ad-unit-3-access-more = Eikekuaave
# ad 4 heading
ad-unit-4-shopping-with = Embaejogua tovaraãnga reheve ñanduti veve rupive
ad-unit-4-want-to-buy = Embaejoguasépa ñandutí rupive ha ndereikuaái jejoguaha térã nderejeroviái hese
ad-unit-4-shop-online = Eipuru ñanduti veve rovaraãnga embaejoguávo ñanduti rupive. Erekóta ñemoneĩ ne ñanduti vevépe ha upéi eipea tovaraãnga ejapose vove.
ad-unit-4-shop-online = Eiporu ñanduti veve rovaraãnga embaejoguávo ñanduti rupive. Erekóta ñemoneĩ ne ñanduti vevépe ha upéi eipea tovaraãnga ejapose vove.
# ad 5 heading
ad-unit-5-on-the-go = Pyaeterei { -brand-relay } ndive
ad-unit-5-instantly-make = ¡Ejapo ñanduti veve rovaraãnga oimeraẽva tendápe ha eho ehohápe!
# ad 5 subheading 1
ad-unit-5-connect-on-the-go = Eikékatu pyae porã
ad-unit-5-privately-sign-in = Eipuru ñanduti veve rovaraãnga emoñepyrũnguévo ne rembiapo tekoñemigua ekaruhápe térã Wi-Fi opavave oiporukuaávape
ad-unit-5-privately-sign-in = Eiporu ñanduti veve rovaraãnga emoñepyrũnguévo ne rembiapo tekoñemigua ekaruhápe térã Wi-Fi opavave oiporukuaávape
# ad 5 subheading 2
ad-unit-5-email-receipts = Og̃uahẽ kuatia ñanduti veve rupive
ad-unit-5-share-custom-email = Emoherakuã peteĩ ñanduti veve rovaraãnga nembaeteéva kuatia ñembaejoguakue emeỹ rehe ne ñanduti veve teete
# ad 5 subheading 3
ad-unit-5-use-on-phone = Eipuru ne pumbyrýpe
ad-unit-5-use-on-phone = Eiporu ne pumbyrýpe
ad-unit-5-no-matter-where = Tereime reimehápe, emoheñói peteĩ ñanduti veve rovaraãnga nembaeteéva sapyaitépe ejapo hag̃ua ejaposéva uperire
# ad 6 heading
ad-unit-6-worry-free = Eñemboheraguapy apañuãiỹre
ad-unit-6-want-to-start = Eñemboheraguapysépa, embohovái pe ñepepirũ térã ejerurekuaa ñemyasãirã ayvu pe spam oñuãỹre ne ñeẽmondo g̃uahẽha
ad-unit-6-before-you-complete = Emoĩmba mboyve pe jehaipy oĩtava, eipuru ñanduti veve rovaraãnga ne ñanduti vevetéva rendaguépe emoã hag̃ua ne marandu ha ehechamemekuaa hag̃ua ne ñeẽmondo g̃uahẽha
ad-unit-6-before-you-complete = Emoĩmba mboyve pe jehaipy oĩtava, eiporu ñanduti veve rovaraãnga ne ñanduti vevetéva rendaguépe emoã hag̃ua ne marandu ha ehechamemekuaa hag̃ua ne ñeẽmondo g̃uahẽha
# Monitor V2
@ -721,7 +721,7 @@ site-nav-breaches-link = Emoĩporã mbaekuaarã ñembogua
site-nav-settings-link = Ñemboheko
site-nav-help-link = Ñepytvõ ha Pytyvõha
# This call-out is above 2 image links for Firefox Relay and Mozilla VPN
site-nav-ad-callout = Eipuru ore ambue rembipuru tekorosãrã:
site-nav-ad-callout = Eiporu ore ambue rembiporu tekorosãrã:
brand-relay = { -brand-relay }
brand-mozilla-vpn = { -brand-mozilla-vpn }

Просмотреть файл

@ -12,7 +12,7 @@ breach-detail-meta-title = { -brand-fx-monitor } - { $company } Mbaekuaarã
# Variables:
# $company (String) - Name of the company that was breached, e.g. "PHP Freaks"
breach-detail-meta-social-title = ¿Ne myangekói { $company } mbaekuaarã ñembogua?
breach-detail-meta-social-description = Eipuru { -brand-fx-monitor } eikuaa hag̃ua ne maranduetépa oñemboguakuaárae ha péicha rupi eikuaa mbaetépa ejapóta.
breach-detail-meta-social-description = Eiporu { -brand-fx-monitor } eikuaa hag̃ua ne maranduetépa oñemboguakuaárae ha péicha rupi eikuaa mbaetépa ejapóta.
breach-scan-meta-title = { -brand-fx-monitor } - Ñembogua Rehegua
breach-scan-meta-social-title = { -brand-fx-monitor } - Ñembogua Rehegua
breach-scan-meta-social-description = Eñepyrũ tembiapo { -brand-fx-monitor } ndive emoĩporã hag̃ua ñembogua ha ereko jehechameme ñembogua pyahu rovake.
@ -87,7 +87,7 @@ breach-checklist-email-body = Kóva oñomikuaa ne ñanduti veve kundaharape omon
# Credit reports list your bill payment history, loans, current debt, and other financial information.
# They show where you work and live and whether you've been sued, arrested, or filed for bankruptcy.
breach-checklist-ssn-header = Ehechameme ne mbaete reko banco pegua, virujepuru térã kuatiaatã ñemurã emoneĩỹva.
breach-checklist-ssn-header = Ehechameme ne mbaete reko banco pegua, virujeporu térã kuatiaatã ñemurã emoneĩỹva.
# A security freeze prevents prospective creditors from accessing your credit file.
# Creditors typically won't offer you credit if they can't access your credit reporting file,
# so a security freeze, also called a credit freeze, prevents you or others from opening accounts in your name.
@ -105,7 +105,7 @@ breach-checklist-cc-body = Avei ehechajey nde kuatiaatã ñemurã reko eikuaa
## Prompts the user for changes when there is a breach detected of bank account
breach-checklist-bank-header = Emomarandu pyae banco-pe ne mbaete papapy ojepurukuaátaramo.
breach-checklist-bank-header = Emomarandu pyae banco-pe ne mbaete papapy ojeporukuaátaramo.
breach-checklist-bank-body = Ejapo pyaevéramo ikatu ne moãve nepytyvõkuaáva oimeraẽva mbaevaígui. Avei ehechajeykuaa ne mbaete ehekahápe oimeraẽva mbae nembaeỹva.
## Prompts the user for changes when there is a breach detected of pin
@ -117,13 +117,13 @@ breach-checklist-pin-body = Aníke ne PIN pyahu, térã oimeraẽva ambue PIN, n
# Variables:
# $mozillaVpnLink (string) - a link to the Mozilla VPN website, with { -breach-checklist-link-mozilla-vpn } as the label
breach-checklist-ip-header-2 = Eipuru ñanduti tekoñemíme VPN ndive, { $mozillaVpnLink } ojapoháicha.
breach-checklist-ip-header-2 = Eiporu ñanduti tekoñemíme VPN ndive, { $mozillaVpnLink } ojapoháicha.
breach-checklist-ip-body = Nde IP kundaharape (Ñanduti rapereko kundaharape) ohechauka ne rendaite ha Ñanduti mbaepuru meẽhára. VPN omokañykuaa nde IP kundaharape eiporukuaa hag̃ua Ñanduti teko ñemíme.
## Prompts the user for changes when there is a breach detected of physical address
breach-checklist-address-header = Emoambue oimeraẽva ñeẽñemi térã PIN orekóva nde kundaharape vore.
breach-checklist-address-body = Kundaharape ndahasýi ijuhu teraguapy opavaveguápe ha ikatu ajapo koã ñeẽñemi ha PINS hasyỹ hag̃ua ijepuru.
breach-checklist-address-body = Kundaharape ndahasýi ijuhu teraguapy opavaveguápe ha ikatu ajapo koã ñeẽñemi ha PINS hasyỹ hag̃ua ijeporu.
## Prompts the user for changes when there is a breach detected of date of birth
@ -142,7 +142,10 @@ breach-checklist-sq-header-text = Embopyahu porandu tekorosãgua.
## Prompts the user for changes when there is a breach detected of historical password
breach-checklist-hp-header = Emoheñói mbaete hekorosã ha haeñóva oimeraẽva mbaétépe g̃uarã eipurujeyhague ñeẽñemi.
breach-checklist-hp-header = Emoheñói mbaete hekorosã ha haeñóva oimeraẽva mbaétépe g̃uarã eiporujeyhague ñeẽñemi.
# Variables:
# $passwordManagerLink (string) - a link to the password manager documentation, with { -breach-checklist-link-password-manager } as the label
breach-checklist-hp-body-2 = Peteĩ ñeẽñemi ñangarekoha ikatúva { $passwordManagerLink } (reigua ha oĩvavoi { -brand-firefox } kundahára ndive) nepytyvõkuaa ehapykueho hag̃ua opaite ñeẽñemi ha eike tekorosãme opaite mbaeoka guive.
## Prompts the user for changes when there is a breach detected of other types

Просмотреть файл

@ -40,7 +40,7 @@ deceased-statuses = Ñemanoha rekotee
delivery-instructions = Delivery jejapo rape
device-information = Mbaeoka marandu
device-serial-numbers = Mbaeoka papapy hekoetáva
device-usage-tracking-data = Mbaeoka jepuru mbaekuaarã rapykuehoha
device-usage-tracking-data = Mbaeoka jeporu mbaekuaarã rapykuehoha
drinking-habits = Guari ñemokõ meme
driver-s-licenses = Monguekuaa ñeẽmeẽguigua
drug-habits = Droga jeu meme
@ -55,7 +55,7 @@ ethnicities = Atygua mbaekuaarã
family-members-names = Pehẽnguekuéra réra
family-plans = Pehẽnguekuéra aporã
family-structure = Pehẽnguegua jeiko
financial-investments = Pirapire jepuruporã
financial-investments = Pirapire jeporuporã
financial-transactions = Pirapire mohendakuaa
fitness-levels = Tetekue rapykuere
flights-taken = Jevevepyre
@ -65,7 +65,7 @@ government-issued-ids = Tetã Rekuái onohẽva IDs
health-insurance-information = Marandu tesãirã jehepyme
historical-passwords = Ñeẽñemi rembiasakuéva
hiv-statuses = VIH rekotee
home-loan-information = Marandu virujepuru rehegua
home-loan-information = Marandu virujeporu rehegua
home-ownership-statuses = Mbaetee rekotee óga rehegua
homepage-urls = Kuatiarogue ñepyrũha URLs
imei-numbers = IMEI papapy

Просмотреть файл

@ -1,4 +1,5 @@
exposure-landing-hero-heading = Eporandu ne maranduetépa oñembyaikuaárae
exposure-landing-hero-lead = Eñemohekorosã { -brand-firefox } rembiporu ñemigua moheñoihára ndive ne moãtava mbaevaiapohágui ha mbaapohaguasu omoherakuã ha ovendéva ne reheguaite. Romomarandúta mbaekuaarã ñembogua oikóvagui, roheka ha romboguéta ne marandu ivaikuaáva ha romaẽmeméta ñembogua pyahúvare.
exposure-landing-hero-email-label = Ñanduti veve kundaharape
exposure-landing-hero-email-placeholder = Ehai ñanduti veve kundaharape
exposure-landing-hero-cta-label = Ehechajey oĩpa ñembyai
@ -22,5 +23,6 @@ exposure-landing-result-overflow-footer-cta-label = Emoñepyrũjey tembiapo ehec
exposure-landing-result-some-hero-lead = Emoñepyrũ tembiapo ereko hag̃ua jekuerã mbaéichapa emyatyrõta koã ñembyai, ehechapáta ñembyai ha erekóta ñemaẽmeme oimeraẽ ñembyai pyahu rovake.
exposure-landing-result-some-hero-cta-label = Emoñepyrũjey tembiapo oĩporãjey hag̃ua
exposure-landing-result-some-footer-cta-label = Emoñepyrũjey tembiapo oĩporãjey hag̃ua
exposure-landing-result-none-hero-lead = ¡Marandu porã! Ndojejuhúi ñembogua pyahu. Eñemohekorosã ñemboheraguapy rupi og̃uahẽ hag̃ua marandu ñembogua pyahúre. Romaẽta ko ñanduti vevére ha roéta oĩrõ ñembogua pyahu.
exposure-landing-result-none-hero-cta-label = Emog̃uahẽuka mbaevai pyahu rehegua
exposure-landing-result-none-footer-cta-label = Eñemboheraguapy og̃uahẽ hag̃ua ndéve kehyjerã

Просмотреть файл

@ -5,7 +5,7 @@ rec-ssn-cta = Ejerure marandui viruguáva
rec-ssn =
Eguereko mbohapy marandui ñemurã reigua arýpe léipe heiháicha.
ejerure ha ehechajey nombyaíri ne ñemurã.
Eheka mbaete, virujepuru térã kuatiaatã ñemurã eikuaaỹva.
Eheka mbaete, virujeporu térã kuatiaatã ñemurã eikuaaỹva.
# Recommendation subhead
rec-pw-1-subhead = Emoambue ne ñeẽñemi
# Link title
@ -22,14 +22,14 @@ rec-pw-2 =
Eiporujeývo ñeẽñemi ombohetakuaa mbaekuaarã ñembyai. Kóva oikóvo
ñeẽñemi eiporukuaa, umi mbaevai apoha ikatu oiporu oike hag̃ua ambue mbaetépe.
# Recommendation subhead
rec-pw-3-subhead = Eipuru ñeẽñemi ñangarekoha eraha hag̃ua ne ñeẽñemi opa hendápe
rec-pw-3-subhead = Eiporu ñeẽñemi ñangarekoha eraha hag̃ua ne ñeẽñemi opa hendápe
# Link title
rec-pw-3-cta = Ereko { -brand-lockwise }
rec-pw-3-fx =
Eipuru { -brand-lockwise } eike hag̃ua tekorosãme ñeẽñemi eñongatuvaekue
Eiporu { -brand-lockwise } eike hag̃ua tekorosãme ñeẽñemi eñongatuvaekue
{ -brand-name }-pe oimeraẽva tenda guive, ikatu avei kundaháragui okápe.
rec-pw-3-non-fx =
Eipuru { -brand-lockwise } ehapykueho hag̃ua opaite ne
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)
@ -50,24 +50,24 @@ rec-cc =
Emaẽ tapiáke nde kuatiaatã ñemurã. Ikatu hína
ejerure kuatiaatã ñemurã pyahu ipapapy pyahúva meẽhárape.
# Recommendation subhead
rec-email-mask-subhead = Eipuru peteĩ ñanduti veve rovaraãnga
rec-email-cta = Eipuru { -brand-relay }
rec-email-mask-subhead = Eiporu peteĩ ñanduti veve rovaraãnga
rec-email-cta = Eiporu { -brand-relay }
rec-email =
Emeẽvo ne ñanduti veve haetéva remoĩma umi mbaevaiapoha ha tapykuehoha
ojuhúvo ne ñeẽñemi térã ndejuhúvo ñandutípe. Ko { -brand-relay } rembiapo
hae omokañývo ne ñanduti veve emondojey aja ñandutiveve ne g̃uahẽhame.
# Recommendation subhead
rec-ip-subhead-2 = Eipuru VPN emoñemi hag̃ua nde IP kundaharape
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 mbaepuru { -brand-mozilla-vpn } rehegua
omoypytũ nde IP kundaharape oñomi hag̃ua ne rendaite.
rec-hist-pw-subhead = Aníke eipurujoa ñeẽñemi
rec-hist-pw-subhead = Aníke eiporujoa ñeẽñemi
# Link title
rec-hist-pw-cta-fx = Ehecha tembiapo ñepyrũ { -brand-name }
rec-hist-pw = Eipuru ñeẽñemi haete ha hekorosãva peteĩteĩva mbaetépe g̃uarã. Pe ñeẽñemi oreko ijehe mbaekuaarã ñembyai, tekotevẽ embohekopyahu tembiapo ñepyrũ año.
rec-hist-pw = Eiporu ñeẽñemi haete ha hekorosãva peteĩteĩva mbaetépe g̃uarã. Pe ñeẽñemi oreko ijehe mbaekuaarã ñembyai, tekotevẽ embohekopyahu tembiapo ñepyrũ año.
# Recommendation subhead
rec-sec-qa-subhead = Emoheñói mbohovái porandu hekorosãva rehegua
rec-sec-qa =
@ -79,25 +79,25 @@ rec-phone-num =
Ani emeẽ ne pumbyry papapy eñemboheraguapývo mbaete
ipyahúvape térã mbaepururãme. Nereikotevẽiramo pumbyry papapy, anínte emoinge.
# Recommendation subhead
rec-dob-subhead = Aníke eipuru nde jehegua PINs-pe
rec-dob-subhead = Aníke eiporu nde jehegua PINs-pe
rec-dob =
Ndahasýi rupi nereñoiha ára ijuhu,
ani eipuru ñeẽñemi ha PINs. Umi
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. Orekovaerã papapy nde añoite eikuaáva ha ndaikatuiva ojekuaarei.
# Recommendation subhead
rec-address-subhead = Aníke eipuru kundaharape ñeẽñemíme
rec-address-subhead = Aníke eiporu kundaharape ñeẽñemíme
rec-address =
Eipurúrõ kundaharape térã tape eiko hague omokangy ne
ñeẽñemi. Mbaekuaarã ndahasýiva ijuhu oimehápe, péva rupi koã
ñeẽñemi ndahasýi ijekuaa.
# Recommendation subhead
rec-gen-1-subhead = Eipuru ñeẽñemi haeño ha hekorosãva peteĩteĩva mbaetépe g̃uarã
rec-gen-1-subhead = Eiporu ñeẽñemi haeño ha hekorosãva peteĩteĩva mbaetépe g̃uarã
# Link title
rec-gen-1-cta = Mbaéicha emoheñóita ñeẽñemi hekorosãva
rec-gen-1 = Ñeẽñemi eipurujeýrõ ombyaikuaa opaite ne mbaete. Kóva heise pe ñeẽñemi ojehechakuaa, umi mbaevaiapoha oreko mbaeñemi heta mbaete rehegua.
rec-gen-1 = Ñeẽñemi eiporujeýrõ ombyaikuaa opaite ne mbaete. Kóva heise pe ñeẽñemi ojehechakuaa, umi mbaevaiapoha oreko mbaeñemi heta mbaete rehegua.
# Recommendation subhead
rec-gen-2-subhead = Embyaty ñeẽñemi tenda hekorosãvape
# Link title

Просмотреть файл

@ -859,7 +859,7 @@ menu-item-logout = Odhlásiť sa
mozilla = { -brand-Mozilla }
terms-and-privacy = Podmienky a ochrana súkromia
github = { -brand-github }
footer-nav-all-breaches = Všetky únik údajov
footer-nav-all-breaches = Všetky úniky údajov
## Error page

Просмотреть файл

@ -136,7 +136,7 @@ sms-messages = 短信内容
social-connections = 社交关系
social-media-profiles = 社交媒体资料
social-security-numbers = 社会安全号码
spoken-languages = 会讲的语言
spoken-languages = 掌握的语言
spouses-names = 配偶姓名
support-tickets = 技术支持请求
survey-results = 问卷结果

Просмотреть файл

@ -9,10 +9,10 @@ exposure-landing-result-error = 检查数据泄漏情况时出了点问题。请
# $email (string) - The user's email address, used to identify their data in breaches
# $count (number) - Number of data breaches in which the user's data was found
exposure-landing-result-hero-heading = 我们发现 <email>{ $email }</email> 在 <count>{ $count } </count> 次数据泄露中暴露。
exposure-landing-result-card-added = 添加的泄漏
exposure-landing-result-card-added = 事件记录时间
exposure-landing-result-card-data = 泄漏的数据:
exposure-landing-result-card-nothing = 未发现数据泄漏
exposure-landing-result-footer-attribution = <hibp-link>{ -brand-HIBP }</hibp-link> 提供的泄漏数据
exposure-landing-result-footer-attribution = 外泄数据由<hibp-link>{ -brand-HIBP }</hibp-link> 提供
exposure-landing-result-overflow-hero-lead = 登录以获取关于如何解决这些泄漏事件的明确步骤、查看所有泄漏事件并持续监控任何新的已知泄漏事件。
exposure-landing-result-overflow-hero-cta-label = 登录以解决泄漏问题
exposure-landing-result-overflow-footer-cta-label = 登录查看全部
@ -20,5 +20,5 @@ exposure-landing-result-some-hero-lead = 登录以获取关于如何解决这些
exposure-landing-result-some-hero-cta-label = 登录以解决数据泄漏问题
exposure-landing-result-some-footer-cta-label = 登录以解决数据泄漏问题
exposure-landing-result-none-hero-lead = 好消息!没有发现已知的数据泄漏问题。通过注册接收新的数据泄漏警报来保持安全。我们将继续监控此电子邮件地址,如果它出现在新的数据泄漏事件中,就会通知您。
exposure-landing-result-none-hero-cta-label = 在有新的数据外泄事件时,接收警报。
exposure-landing-result-none-hero-cta-label = 接收新外泄事件警报
exposure-landing-result-none-footer-cta-label = 订阅警报

Просмотреть файл

@ -42,7 +42,7 @@ what-is-breach = 数据外泄到底是什么?
when-info-exposed = 个人和隐私信息未经许可被暴露、盗窃、复制,即为发生数据外泄。这些安全事件可能是由对网站、应用或者存放个人信息的数据库的攻击造成的。数据外泄也可能在无意中发生,例如某人不小心把登录凭据泄露出去。
# question and answer
what-do-i-do = 我刚刚发现自己遭受了数据外泄。我该怎么办?
visit-monitor-to-learn = 访问 { -brand-fx-monitor } 了解数据外泄后的应对措施。为不同账户使用相同密码会给黑客可乘之机,因此请务必为您的所有账户创建强大且唯一的密码。将密码保存在只有您能够访问的安全位置,可以是您存储其他重要文件的地方,也可以使用密码管理器。
visit-monitor-to-learn = 访问 { -brand-fx-monitor } 了解数据外泄后的应对措施。为不同账户使用相同密码会给黑客可乘之机,因此请务必为您的所有账户创建不同且高强度的密码。将密码保存在只有您能够访问的安全位置,可以是您存储其他重要文件的地方,也可以使用密码管理器。
# question and answer
what-gets-exposed = 数据外泄会暴露哪些信息?
depends-on-hackers = 并非所有外泄事件都会泄露同样的信息,这具体取决于黑客取得了哪些信息。许多数据外泄会泄露电子邮件地址和密码,有些还会泄露更敏感的信息,例如信用卡号、护照号和社保号。

1017
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -9,7 +9,7 @@
"type": "module",
"scripts": {
"dev": "next dev --port=6060",
"build": "npm run get-location-data && next build",
"build": "npm run get-location-data && npm run build-glean && next build",
"start": "next start",
"lint": "stylelint '**/*.scss' && prettier --check './src' && next lint --max-warnings=0",
"fix": "prettier --write './src' && next lint --fix && stylelint --fix '**/*.scss'",
@ -25,7 +25,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"create-location-data": "node src/scripts/uploadAutoCompleteLocations.js",
"get-location-data": "node src/scripts/getAutoCompleteLocations.js"
"get-location-data": "node src/scripts/getAutoCompleteLocations.js",
"build-glean": "glean translate src/telemetry/metrics.yaml --format javascript --output src/telemetry/generated"
},
"nodemonConfig": {
"watch": [
@ -53,7 +54,10 @@
"@fluent/bundle": "^0.17.1",
"@fluent/langneg": "^0.6.2",
"@fluent/react": "^0.15.0",
"@google-cloud/pubsub": "^4.0.3",
"@grpc/grpc-js": "1.8.21",
"@leeoniya/ufuzzy": "^1.0.8",
"@mozilla/glean": "^2.0.2",
"@sentry/nextjs": "^7.58.1",
"@sentry/node": "^7.58.1",
"@sentry/tracing": "^7.58.1",

0
requirements.txt Normal file
Просмотреть файл

Просмотреть файл

@ -101,15 +101,14 @@ export function createRandomBreach(
faker.seed(options.fakerSeed);
return {
addedDate:
options.addedDate?.toISOString() ?? faker.date.recent().toISOString(),
breachDate: faker.date.recent().toISOString(),
addedDate: options.addedDate ?? faker.date.recent(),
breachDate: faker.date.recent(),
dataClasses: dataClasses,
description: faker.word.words(),
domain: faker.internet.domainName(),
id: faker.number.int(),
favIconUrl: faker.system.fileName(),
modifiedDate: faker.date.recent().toISOString(),
modifiedDate: faker.date.recent(),
name: faker.word.noun(),
title: faker.word.noun(),
isResolved: options.isResolved ?? faker.datatype.boolean(),

Просмотреть файл

@ -13,6 +13,7 @@ import LockImage from "../../../client/images/landing-lock@2x.webp";
import MailImage from "../../../client/images/landing-mail@2x.webp";
import NaturePhoneImage from "../../../client/images/landing-nature-phone@2x.webp";
import { getNonce } from "../functions/server/getNonce";
import { PageLoadEvent } from "../components/client/PageLoadEvent";
export default function Home() {
const l10n = getL10n();
@ -34,6 +35,7 @@ export default function Home() {
src="/nextjs_migration/client/js/landing.js"
nonce={getNonce()}
/>
<PageLoadEvent />
<section className="hero">
<div>
<h1>{l10n.getString("exposure-landing-hero-heading")}</h1>

Просмотреть файл

@ -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/. */
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { useGlean } from "../../../hooks/useGlean";
// Empty component that records a page view on first load.
export const PageLoadEvent = () => {
const { pageEvents } = useGlean();
const pathname = usePathname();
// On first load of the page, record a page view.
useEffect(() => {
pageEvents.view.record({ path: pathname });
}, [pageEvents.view, pathname]);
// This component doesn't render anything.
return <></>;
};

Просмотреть файл

@ -16,7 +16,7 @@ import HelpIcon from "../../../../client/images/icon-help.svg";
import SignOutIcon from "../../../../client/images/icon-signout.svg";
export type Props = {
session: Session | null;
session: Session;
fxaSettingsUrl: string;
nonce: string | undefined;
};

Просмотреть файл

@ -32,6 +32,7 @@ const BreachMockItem1: SubscriberBreach = createRandomBreach({
{ "phone-numbers": 1 },
{ passwords: 1 },
],
isResolved: true,
});
const BreachMockItem2: SubscriberBreach = createRandomBreach({
@ -41,6 +42,7 @@ const BreachMockItem2: SubscriberBreach = createRandomBreach({
{ "email-addresses": ["email1@gmail.com", "email2@gmail.com"] },
{ "ip-addresses": 1 },
],
isResolved: false,
});
const BreachMockItem3: SubscriberBreach = createRandomBreach({
@ -61,10 +63,13 @@ const BreachMockItem4: SubscriberBreach = createRandomBreach({
],
});
const scannedResultsArraySample: ScanResult[] = Array.from(
{ length: 5 },
createRandomScan
);
const scannedResultsArraySample: ScanResult[] = [
createRandomScan({ status: "removed" }),
createRandomScan({ status: "waiting_for_verification" }),
createRandomScan({ status: "optout_in_progress" }),
createRandomScan({ status: "new" }),
createRandomScan(),
];
const scannedResolvedResultsArraySample: ScanResult[] = Array.from(
{ length: 5 },
@ -300,6 +305,7 @@ export const DashboardFreeUserAllResolved: Story = {
userScannedResults={scannedResolvedResultsArraySample}
isEligibleForFreeScan={true}
locale={"en"}
isAllFixed={true}
bannerData={dashboardSummaryWithScan}
featureFlagsEnabled={{
FreeBrokerScan: true,
@ -335,23 +341,3 @@ export const DashboardPremiumUser: Story = {
);
},
};
export const DashboardNoSession: Story = {
render: () => (
<Shell l10n={getEnL10nSync()} session={null}>
<DashboardEl
countryCode="us"
user={{ email: "example@example.com" }}
userBreaches={breachItemArraySample}
userScannedResults={scannedResultsArraySample}
isEligibleForFreeScan={false}
locale={"en"}
bannerData={dashboardSummaryWithScan}
featureFlagsEnabled={{
FreeBrokerScan: true,
PremiumBrokerRemoval: true,
}}
/>
</Shell>
),
};

Просмотреть файл

@ -14,7 +14,6 @@ import Meta, {
DashboardWithoutScanUserFromUs,
DashboardFreeUser,
DashboardPremiumUser,
DashboardNoSession,
DashboardFreeUserAllResolved,
} from "./Dashboard.stories";
@ -193,18 +192,6 @@ it("shows the premium badge if the user is a premium subscriber", () => {
expect(premiumBadges.length).toBe(2);
});
it("shows the premium upgrade cta if there is no user session", () => {
enablePremium();
const ComposedDashboard = composeStory(DashboardNoSession, Meta);
render(<ComposedDashboard />);
// We show a CTA on desktop in the toolbar and in the mobile menu
const premiumCtas = screen.queryAllByRole("button", {
name: "Upgrade to Premium",
});
expect(premiumCtas.length).toBe(2);
});
it("shows returned free user who has resolved all tasks premium upsell and all fixed description", async () => {
enablePremium();
const user = userEvent.setup();

Просмотреть файл

@ -150,6 +150,7 @@ export const DashboardTopBanner = (props: DashboardTopBannerProps) => {
// do something
}}
small
disabled
variant="primary"
>
{l10n.getString("dashboard-top-banner-lets-keep-protecting-cta")}

Просмотреть файл

@ -22,13 +22,14 @@ import {
} from "../../../../../components/client/ExposuresFilter";
import { ScanResult } from "../../../../../functions/server/onerep";
import { DashboardSummary } from "../../../../../functions/server/dashboard";
import { StatusPillType } from "../../../../../components/server/StatusPill";
import { getExposureStatus } from "../../../../../components/server/StatusPill";
import { TabList } from "../../../../../components/client/TabList";
import AllFixedLogo from "./images/dashboard-all-fixed.svg";
import { FeatureFlagsEnabled } from "../../../../../functions/server/featureFlags";
import { filterExposures } from "./filterExposures";
import { SubscriberBreach } from "../../../../../../utils/subscriberBreaches";
import { hasPremium } from "../../../../../functions/universal/user";
import { parseIso8601Datetime } from "../../../../../../utils/parse";
export type Props = {
bannerData: DashboardSummary;
@ -42,6 +43,7 @@ export type Props = {
userScannedResults: ScanResult[];
isEligibleForFreeScan: boolean;
countryCode?: string;
isAllFixed?: boolean;
};
export type TabType = "action-needed" | "fixed";
@ -67,13 +69,7 @@ export const View = (props: Props) => {
];
const isActionNeededTab = selectedTab === "action-needed";
const dateObject = (isoString: string): Date => {
return new Date(isoString);
};
const breachesDataArray = props.userBreaches
.map((elem: SubscriberBreach) => elem)
.flat();
const breachesDataArray = props.userBreaches.flat();
const scannedResultsDataArray =
// TODO: Add unit test when changing this code:
/* c8 ignore next */
@ -85,37 +81,18 @@ export const View = (props: Props) => {
// Sort in descending order
const arraySortedByDate = combinedArray.sort((a, b) => {
const dateA =
(a as SubscriberBreach).addedDate || (a as ScanResult).created_at;
(a as SubscriberBreach).addedDate ||
parseIso8601Datetime((a as ScanResult).created_at);
const dateB =
(b as SubscriberBreach).addedDate || (b as ScanResult).created_at;
(b as SubscriberBreach).addedDate ||
parseIso8601Datetime((b as ScanResult).created_at);
const timestampA = new Date(dateA).getTime();
const timestampB = new Date(dateB).getTime();
const timestampA = dateA.getTime();
const timestampB = dateB.getTime();
return timestampB - timestampA;
});
const getExposureStatus = (exposure: Exposure): StatusPillType => {
if (isScanResult(exposure)) {
switch (exposure.status) {
// TODO: Add unit test when changing this code:
/* c8 ignore next 2 */
case "removed":
return "fixed";
// TODO: Add unit test when changing this code:
/* c8 ignore next 2 */
case "waiting_for_verification":
return "progress";
default:
return "needAction";
}
}
// TODO: Add unit test when changing this code:
/* c8 ignore next */
return exposure.isResolved ? "fixed" : "needAction";
};
const tabSpecificExposures = arraySortedByDate.filter(
(exposure: Exposure) => {
const exposureStatus = getExposureStatus(exposure);
@ -128,34 +105,17 @@ export const View = (props: Props) => {
const filteredExposures = filterExposures(tabSpecificExposures, filters);
const exposureCardElems = filteredExposures.map((exposure: Exposure) => {
const status = getExposureStatus(exposure);
return isScanResult(exposure) ? (
// Scanned result
<li key={`scan-${exposure.id}`} className={styles.exposureListItem}>
return (
<li
key={`${isScanResult(exposure) ? "scan" : "breach"}-${exposure.id}`}
className={styles.exposureListItem}
>
<ExposureCard
exposureData={exposure}
exposureName={exposure.data_broker}
exposureDetailsLink={exposure.link}
dateFound={dateObject(exposure.created_at)}
statusPillType={status}
locale={props.locale}
color={getRandomLightNebulaColor(exposure.data_broker)}
featureFlagsEnabled={props.featureFlagsEnabled}
/>
</li>
) : (
// Breaches result
<li key={`breach-${exposure.id}`} className={styles.exposureListItem}>
<ExposureCard
exposureData={exposure}
exposureName={exposure.title}
exposureDetailsLink={`/breach-details/${exposure.name}`}
dateFound={dateObject(exposure.addedDate)}
statusPillType={status}
locale={props.locale}
color={getRandomLightNebulaColor(exposure.name)}
featureFlagsEnabled={props.featureFlagsEnabled}
isPremiumBrokerRemovalEnabled={
props.featureFlagsEnabled.PremiumBrokerRemoval
}
/>
</li>
);
@ -199,7 +159,7 @@ export const View = (props: Props) => {
if (isScanResultItemsEmpty) {
contentType = "DataBrokerScanUpsellContent";
} else if (
!noUnresolvedExposures &&
(!noUnresolvedExposures || !props.isAllFixed) &&
props.countryCode &&
props.countryCode.toLocaleLowerCase() === "us"
) {
@ -213,6 +173,7 @@ export const View = (props: Props) => {
props.countryCode?.toLocaleLowerCase() === "us" &&
noUnresolvedExposures &&
!isScanResultItemsEmpty &&
props.isAllFixed &&
!hasPremium(props.user)
) {
contentType = "YourDataIsProtectedAllFixedContent";
@ -288,53 +249,3 @@ export const View = (props: Props) => {
</div>
);
};
// Same logic as breachLogo.js
function getRandomLightNebulaColor(name: string) {
const colors = [
"#C689FF",
"#D9BFFF",
"#AB71FF",
"#E7DFFF",
"#AB71FF",
"#3FE1B0",
"#54FFBD",
"#88FFD1",
"#B3FFE3",
"#D1FFEE",
"#F770FF",
"#F68FFF",
"#F6B8FF",
"#00B3F4",
"#00DDFF",
"#80EBFF",
"#FF8450",
"#FFA266",
"#FFB587",
"#FFD5B2",
"#FF848B",
"#FF9AA2",
"#FFBDC5",
"#FF8AC5",
"#FFB4DB",
];
const charValues = name.split("").map((letter) => letter.codePointAt(0));
const charSum = charValues.reduce((sum: number | undefined, codePoint) => {
// TODO: Add unit test when changing this code:
/* c8 ignore next */
if (codePoint === undefined) return sum;
if (sum === undefined) return codePoint;
return sum + codePoint;
}, undefined);
// TODO: Add unit test when changing this code:
/* c8 ignore next 3 */
if (charSum === undefined) {
return colors[0];
}
const colorIndex = charSum % colors.length;
return colors[colorIndex];
}

Просмотреть файл

@ -0,0 +1,145 @@
/* 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 Link from "next/link";
import Image from "next/image";
import { ReactNode } from "react";
import { FixNavigation } from "../../../../../../components/client/FixNavigation";
import styles from "./fix.module.scss";
import ImageArrowLeft from "./images/icon-arrow-left.svg";
import ImageArrowRight from "./images/icon-arrow-right.svg";
import ImageClose from "./images/icon-close.svg";
import stepDataBrokerProfilesIcon from "./images/step-counter-data-broker-profiles.svg";
import stepHighRiskDataBreachesIcon from "./images/step-counter-high-risk.svg";
import stepLeakedPasswordsIcon from "./images/step-counter-leaked-passwords.svg";
import stepSecurityRecommendationsIcon from "./images/step-counter-security-recommendations.svg";
import { usePathname } from "next/navigation";
import { GuidedExperienceBreaches } from "../../../../../../functions/server/getUserBreaches";
import { ScanResult } from "../../../../../../functions/server/onerep";
import { useL10n } from "../../../../../../hooks/l10n";
export type FixViewProps = {
children: ReactNode;
breaches: GuidedExperienceBreaches;
userScannedResults: ScanResult[];
};
export const FixView = (props: FixViewProps) => {
const pathname = usePathname();
const l10n = useL10n();
const isResolutionLayout = [
"high-risk-data-breach",
"leaked-passwords",
"security-recommendations",
].some((substring) => pathname.includes(substring));
const totalHighRiskBreaches = Object.values(props.breaches.highRisk).reduce(
(acc, array) => acc + array.length,
0
);
const totalDataBrokerProfiles = props.userScannedResults.length;
const totalPasswordBreaches = Object.values(
props.breaches.passwordBreaches
).reduce((acc, array) => acc + array.length, 0);
const totalSecurityRecommendations = Object.values(
props.breaches.securityRecommendations
).filter((value) => {
return value.length > 0;
}).length;
const navigationItemsContent = [
{
key: "data-broker-profiles",
labelStringId: "fix-flow-nav-data-broker-profiles",
href: "/redesign/user/dashboard/fix/data-broker-profiles",
status: totalDataBrokerProfiles,
currentStepId: "dataBrokerProfiles",
imageId: stepDataBrokerProfilesIcon,
},
{
key: "high-risk-data-breaches",
labelStringId: "fix-flow-nav-high-risk-data-breaches",
href: "/redesign/user/dashboard/fix/high-risk-data-breaches",
status: totalHighRiskBreaches,
currentStepId: "highRiskDataBreaches",
imageId: stepHighRiskDataBreachesIcon,
},
{
key: "leaked-passwords",
labelStringId: "fix-flow-nav-leaked-passwords",
href: "/redesign/user/dashboard/fix/leaked-passwords",
status: totalPasswordBreaches,
currentStepId: "leakedPasswords",
imageId: stepLeakedPasswordsIcon,
},
{
key: "security-recommendations",
labelStringId: "fix-flow-nav-security-recommendations",
href: "/redesign/user/dashboard/fix/security-recommendations",
status: totalSecurityRecommendations,
currentStepId: "securityRecommendations",
imageId: stepSecurityRecommendationsIcon,
},
];
const navigationClose = () => {
return (
<Link
href="/redesign/user/dashboard"
className={styles.navClose}
aria-label={l10n.getString("guided-resolution-flow-exit")}
>
<Image alt="" src={ImageClose} />
</Link>
);
};
// TODO: MNTOR-1700 Add routing logic here
const navigationArrowBack = () => {
return (
<Link
className={styles.navArrowBack}
href="/redesign/user/dashboard"
aria-label={l10n.getString("guided-resolution-flow-back-arrow")}
>
<Image alt="" src={ImageArrowLeft} />
</Link>
);
};
// TODO: MNTOR-1700 Add routing logic here
const navigationArrowNext = () => {
return (
<Link
className={styles.navArrowNext}
href="/redesign/user/dashboard"
aria-label={l10n.getString("guided-resolution-flow-next-arrow")}
>
<Image alt="" src={ImageArrowRight} />
</Link>
);
};
return (
<div className={styles.fixContainer}>
<div
className={`${styles.fixWrapper} ${
isResolutionLayout ? styles.highRiskDataBreachContentBg : ""
}`}
>
<FixNavigation
navigationItems={navigationItemsContent}
pathname={pathname}
/>
{navigationClose()}
<section className={styles.fixSection}>
{navigationArrowBack()}
{navigationArrowNext()}
{props.children}
</section>
</div>
</div>
);
};

Просмотреть файл

@ -15,12 +15,24 @@
flex-direction: column;
gap: $spacing-sm;
.label {
align-self: flex-start;
background: rgba($color-blue-50, 0.3);
border-radius: $border-radius-sm;
color: $color-blue-90;
display: inline-flex;
font: $text-body-xs;
font-weight: 600;
line-height: 1;
padding: $spacing-sm;
}
h3 {
font: $text-title-2xs;
padding: $spacing-sm 0 $spacing-xs;
}
.recommendations {
padding-top: $spacing-md;
p {
font: $text-body-sm;
}
@ -29,6 +41,7 @@
list-style-position: inside;
padding-left: 0;
}
padding-top: $spacing-md;
}
.buttons {
@ -58,13 +71,8 @@
}
.estimatedTime {
display: flex;
flex-direction: row;
gap: $spacing-xs;
align-items: center;
font: $text-body-xs;
font-weight: 500;
padding-top: $spacing-md;
@include estimated-time;
padding-top: $layout-xs;
}
.illustrationWrapper {

Просмотреть файл

@ -4,63 +4,41 @@
"use client";
import styles from "./ResolutionContentLayout.module.scss";
import Image from "next/image";
import { Button } from "../../../../../../components/server/Button";
import Link from "next/link";
import type { ReactNode } from "react";
import { ClockIcon } from "../../../../../../components/server/Icons";
import { ReactNode } from "react";
import { useL10n } from "../../../../../../hooks/l10n";
import styles from "./ResolutionContainer.module.scss";
type ResolutionContentLayoutProps = {
type ResolutionContainerProps = {
type: "highRisk" | "leakedPasswords" | "securityRecommendations";
title: string;
illustration: string;
cta: {
label: string;
onClick: () => void;
skip?: string;
};
estimatedTime?: number;
children: ReactNode;
label?: string;
cta?: ReactNode;
};
// TODO: Add test once routes from MNTOR-1700 is available
/* c8 ignore start */
export const ResolutionContentLayout = (
props: ResolutionContentLayoutProps
) => {
export const ResolutionContainer = (props: ResolutionContainerProps) => {
const l10n = useL10n();
const estimatedTimeString =
props.type === "leakedPasswords"
? "leaked-passwords-estimated-time"
: "high-risk-breach-estimated-time";
return (
// TODO: Check with design if toolbar should be on this page
<div className={styles.container}>
<div className={styles.breachContentWrapper}>
{props.label && <div className={styles.label}>{props.label}</div>}
<h3>{props.title}</h3>
{props.children}
<div className={styles.buttons}>
<Button
variant="primary"
small
onClick={() => {
props.cta.onClick;
}}
>
{props.cta.label}
</Button>
{props.cta.skip && (
<Link
// TODO: MNTOR-1700 Add routing logic here
href={props.cta.skip}
>
{l10n.getString("high-risk-breach-skip")}
</Link>
)}
</div>
{props.cta && <div className={styles.buttons}>{props.cta}</div>}
{props.estimatedTime && (
<div className={styles.estimatedTime}>
<ClockIcon width="20" height="20" alt="" />
{l10n.getString("high-risk-breach-estimated-time", {
{l10n.getString(estimatedTimeString, {
estimated_time: props.estimatedTime,
})}
</div>
@ -72,4 +50,3 @@ export const ResolutionContentLayout = (
</div>
);
};
/* c8 ignore stop */

Просмотреть файл

@ -0,0 +1,52 @@
@import "../../../../../../tokens";
.recommendations {
padding-top: $spacing-md;
p {
font: $text-body-sm;
}
ul,
ol {
padding-top: $spacing-md;
list-style: none;
list-style-position: inside;
padding-left: 0;
counter-reset: item;
/* We're using 'counter-increment' and '::before' to manually add list item numbers instead of default list style styling.
This approach allows us to customize the appearance of the numbers and maintain control
over their positioning while working with 'display: flex' for list items.
*/
li {
counter-increment: item;
margin-bottom: $spacing-xs;
display: flex;
align-items: flex-start;
gap: $spacing-xs;
&::before {
content: counter(item) ". ";
margin-right: $spacing-xs;
}
}
}
}
.breachItemsWrapper {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
.breachItem {
font: $text-body-sm;
font-weight: 600;
background: $color-grey-10;
padding: $spacing-xs $spacing-sm;
border-radius: $border-radius-md;
.date {
font-weight: 400;
}
}
}

Просмотреть файл

@ -0,0 +1,60 @@
/* 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 type { SecurityRecommendationContent } from "./security-recommendations/securityRecommendationsData";
import type { SubscriberBreach } from "../../../../../../../utils/subscriberBreaches";
import { useL10n } from "../../../../../../hooks/l10n";
import styles from "./ResolutionContent.module.scss";
interface ResolutionContentProps {
content: SecurityRecommendationContent;
exposedData?: SubscriberBreach[];
locale: string;
}
export const ResolutionContent = ({
content,
exposedData,
locale,
}: ResolutionContentProps) => {
const l10n = useL10n();
const { summary, description, recommendations } = content;
const dateFormatter = new Intl.DateTimeFormat(locale, {
dateStyle: "short",
});
const listOfBreaches =
exposedData &&
exposedData.map(({ id, title, breachDate }) => (
<div key={id} className={styles.breachItem}>
{l10n.getFragment("high-risk-breach-name-and-date", {
elems: { breach_date: <span className={styles.date} /> },
vars: {
breach_name: title,
breach_date: dateFormatter.format(breachDate),
},
})}
</div>
));
return (
<>
<p>{summary}</p>
{exposedData && (
<div className={styles.breachItemsWrapper}>{listOfBreaches}</div>
)}
{description}
{recommendations && (
<div className={styles.recommendations}>
<h4>{recommendations.title}</h4>
{recommendations.subtitle && <p>{recommendations.subtitle}</p>}
{recommendations.steps}
</div>
)}
</>
);
};

Просмотреть файл

@ -192,3 +192,19 @@
}
}
}
.dataBrokerResolutionStats {
display: flex;
flex-direction: column;
gap: $spacing-md;
justify-content: center;
padding-top: $layout-sm;
@media screen and (min-width: $screen-md) {
flex-direction: row;
}
div {
@include estimated-time;
justify-content: center;
}
}

Просмотреть файл

@ -2,13 +2,44 @@
* 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 { useL10n } from "../../../../../../../../hooks/l10n";
import styles from "../dataBrokerProfiles.module.scss";
import buttonStyles from "../../../../../../../../components/server/button.module.scss";
import Link from "next/link";
import { getL10n } from "../../../../../../../../functions/server/l10n";
import {
AvatarIcon,
ClockIcon,
} from "../../../../../../../../components/server/Icons";
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
import { getLatestOnerepScan } from "../../../../../../../../../db/tables/onerep_scans";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { dashboardSummary } from "../../../../../../../../functions/server/dashboard";
export const ManualRemoveView = () => {
const l10n = useL10n();
export default async function ManualRemoveView() {
const l10n = getL10n();
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
redirect("/redesign/user/dashboard/");
}
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] as number;
const scanResult = await getLatestOnerepScan(profileId);
const scanResultItems = scanResult?.onerep_scan_results?.data ?? [];
const subBreaches = await getSubscriberBreaches(session.user);
const summary = dashboardSummary(scanResultItems, subBreaches);
// TODO: Use api to set/query count
const countOfDataBrokerProfiles = scanResultItems.length;
const estimatedTime = countOfDataBrokerProfiles * 10; // 10 minutes per data broker site.
const totalExposures = summary.totalExposures;
const exposureReduction = Math.round(
(countOfDataBrokerProfiles / totalExposures) * 100
);
return (
<div className={styles.main}>
@ -95,6 +126,20 @@ export const ManualRemoveView = () => {
)}
</Link>
</div>
<div className={styles.dataBrokerResolutionStats}>
<div>
<ClockIcon width="18" height="18" alt="" />
{l10n.getString("data-broker-profiles-estimated-time", {
estimated_time: estimatedTime,
})}
</div>
<div>
<AvatarIcon width="18" height="18" alt="" />
{l10n.getString("data-broker-profiles-exposure-reduction", {
exposure_reduction: exposureReduction,
})}
</div>
</div>
</div>
);
};
}

Просмотреть файл

@ -2,8 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ManualRemoveView } from "./ManualRemoveView";
import ManualRemoveView from "./ManualRemoveView";
export default function ManualRemove() {
return <ManualRemoveView />;
return <>{ManualRemoveView()}</>;
}

Просмотреть файл

@ -28,6 +28,11 @@ export const AboutBrokersIcon = () => {
const triggerRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(triggerProps, triggerRef);
const dataBrokerCount = parseInt(
process.env.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
10
);
return (
<>
<button
@ -63,7 +68,10 @@ export const AboutBrokersIcon = () => {
</p>
<p>
{l10n.getString(
"fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-paragraph2"
"fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-paragraph2",
{
data_broker_sites_total_num: dataBrokerCount,
}
)}
</p>
<div className={styles.confirmButtonWrapper}>

Просмотреть файл

@ -21,7 +21,7 @@
position: relative;
background-image: url("./images/content-background.svg");
&.highRiskDataBreachContentBg {
&.contentBackgroundImage {
background-image: url("./images/high-risk-breaches-content-background.svg");
}
}
@ -40,11 +40,11 @@
@media screen and (min-width: $screen-md) {
// Add horizontal padding to account for next/prev arrow placement on larger screens
padding: 0 $spacing-2xl;
padding: $spacing-lg $spacing-2xl;
}
@media screen and (min-width: $screen-xl) {
padding: 0;
padding: $spacing-xl $spacing-lg $spacing-md $spacing-lg;
}
.navArrowBack {

Просмотреть файл

@ -0,0 +1,20 @@
@import "../../../../../../../tokens";
.triggerButton {
@include question-mark-circle-btn;
}
.dialogContents {
display: flex;
flex-direction: column;
gap: $spacing-lg;
color: $color-grey-50;
.confirmButtonWrapper {
align-self: center;
display: flex;
flex-direction: column;
min-width: $content-xs;
padding-block-start: $spacing-md;
}
}

Просмотреть файл

@ -0,0 +1,136 @@
/* 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 { useOverlayTriggerState } from "react-stately";
import { useL10n } from "../../../../../../../hooks/l10n";
import { useButton, useOverlayTrigger } from "react-aria";
import { useRef } from "react";
import { QuestionMarkCircle } from "../../../../../../../components/server/Icons";
import { ModalOverlay } from "../../../../../../../components/client/dialog/ModalOverlay";
import { Dialog } from "../../../../../../../components/client/dialog/Dialog";
import Image from "next/image";
import FraudAlertDialogIllustration from "../images/fraud-alert-modal-illustration.svg";
import { Button } from "../../../../../../../components/server/Button";
import styles from "./FraudAlertModal.module.scss";
export const FraudAlertModal = () => {
const l10n = useL10n();
const overlayTriggerState = useOverlayTriggerState({
defaultOpen: false,
});
const { triggerProps, overlayProps } = useOverlayTrigger(
{ type: "dialog" },
overlayTriggerState
);
const triggerRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(triggerProps, triggerRef);
const fraudAlertLink =
"https://consumer.ftc.gov/articles/what-know-about-credit-freezes-fraud-alerts";
const equifaxLink =
"https://www.equifax.com/personal/credit-report-services/";
const experianLink = "https://www.experian.com/help/";
const transunionLink = "https://www.transunion.com/credit-help";
return (
<>
<button
{...buttonProps}
ref={triggerRef}
className={styles.triggerButton}
>
<QuestionMarkCircle
alt={l10n.getString(
"fix-flow-data-broker-profiles-view-data-broker-profiles-more-dialog-trigger-label"
)}
width={18}
height={18}
/>
</button>
{overlayTriggerState.isOpen && (
<ModalOverlay
state={overlayTriggerState}
{...overlayProps}
isDismissable={true}
>
<Dialog
title={l10n.getString("ssn-modal-title")}
illustration={<Image src={FraudAlertDialogIllustration} alt="" />}
>
<div className={styles.dialogContents}>
<p>
{l10n.getFragment("ssn-modal-description-fraud-part-one", {
elems: {
b: <strong />,
},
})}
&nbsp;
{l10n.getString("ssn-modal-description-fraud-part-two")}
</p>
<p>
{l10n.getFragment(
"ssn-modal-description-freeze-credit-part-one",
{
elems: {
b: <strong />,
},
}
)}
&nbsp;
{l10n.getFragment(
"ssn-modal-description-freeze-credit-part-two",
{
elems: {
equifax_link: (
<a
href={equifaxLink}
rel="noopener noreferrer"
target="_blank"
/>
),
experian_link: (
<a
href={experianLink}
rel="noopener noreferrer"
target="_blank"
/>
),
transunion_link: (
<a
href={transunionLink}
rel="noopener noreferrer"
target="_blank"
/>
),
},
}
)}
</p>
<p>
<a
href={fraudAlertLink}
rel="noopener noreferrer"
target="_blank"
>
{l10n.getString("ssn-modal-learn-more")}
</a>
</p>
<div className={styles.confirmButtonWrapper}>
<Button
variant="primary"
onPress={() => overlayTriggerState.close()}
autoFocus={true}
>
{l10n.getString("ssn-modal-ok")}
</Button>
</div>
</div>
</Dialog>
</ModalOverlay>
)}
</>
);
};

Просмотреть файл

@ -1,163 +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 CreditCardIllustration from "../images/high-risk-data-breach-credit-card.svg";
import BankAccountIllustration from "../images/high-risk-data-breach-bank-account.svg";
import pinIllustration from "../images/high-risk-data-breach-pin.svg";
import SocialSecurityNumberIllustration from "../images/high-risk-data-breach-ssn.svg";
import NoBreachesIllustration from "../images/high-risk-breaches-none.svg";
import { useL10n } from "../../../../../../../hooks/l10n";
type HighRiskBreachContentProps = {
typeOfBreach: string;
locale: string;
};
export const HighRiskBreachContent = (props: HighRiskBreachContentProps) => {
const l10n = useL10n();
let title, secondaryDescription, recommendationSteps, breachIllustration;
// TODO: Expose email list & count here https://mozilla-hub.atlassian.net/browse/MNTOR-2112
const emailsMonitored = ["email1@gmail.com", "email2@gmail.com"]; // mocked
const emailsFormatter = new Intl.ListFormat(props.locale, {
style: "long",
type: "conjunction",
});
const CreditCardRecommendationSteps = (
<ol>
<li>{l10n.getString("high-risk-breach-credit-card-step-one")}</li>
<li>{l10n.getString("high-risk-breach-credit-card-step-two")}</li>
<li>{l10n.getString("high-risk-breach-credit-card-step-three")}</li>
</ol>
);
const BankAccountRecommendationSteps = (
<ol>
<li>{l10n.getString("high-risk-breach-bank-account-step-one")}</li>
<li>{l10n.getString("high-risk-breach-bank-account-step-two")}</li>
<li>{l10n.getString("high-risk-breach-bank-account-step-three")}</li>
</ol>
);
const stepOneSSNLink =
"https://consumer.ftc.gov/articles/what-know-about-credit-freezes-fraud-alerts";
const stepTwoSSNLink = "https://www.annualcreditreport.com/index.action";
const SocialSecurityNumberRecommendationSteps = (
<ol>
{/* TOOD: Add question mark modal explaining the SSN breach resolution - MNTOR-2127 */}
<li>
{l10n.getFragment("high-risk-breach-social-security-step-one", {
elems: {
link_to_info: (
<a
href={stepOneSSNLink}
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
<li>
{l10n.getFragment("high-risk-breach-social-security-step-two", {
elems: {
link_to_info: (
<a
href={stepTwoSSNLink}
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
</ol>
);
const pinRecommendationSteps = (
<ol>
<li>{l10n.getString("high-risk-breach-pin-step-one")}</li>
<li>{l10n.getString("high-risk-breach-pin-step-two")}</li>
<li>{l10n.getString("high-risk-breach-pin-step-three")}</li>
</ol>
);
switch (props.typeOfBreach) {
case "creditCard":
title = l10n.getString("high-risk-breach-credit-card-title");
secondaryDescription = (
<p>{l10n.getString("high-risk-breach-credit-card-description")}</p>
);
recommendationSteps = CreditCardRecommendationSteps;
breachIllustration = CreditCardIllustration;
break;
case "ssnBreaches":
title = l10n.getString("high-risk-breach-social-security-title");
secondaryDescription = (
<p>{l10n.getString("high-risk-breach-social-security-description")}</p>
);
recommendationSteps = SocialSecurityNumberRecommendationSteps;
breachIllustration = SocialSecurityNumberIllustration;
break;
case "bankAccount":
title = l10n.getString("high-risk-breach-bank-account-title");
secondaryDescription = (
<p>{l10n.getString("high-risk-breach-bank-account-description")}</p>
);
recommendationSteps = BankAccountRecommendationSteps;
breachIllustration = BankAccountIllustration;
break;
case "pin":
title = l10n.getString("high-risk-breach-pin-title");
secondaryDescription = (
<p>{l10n.getString("high-risk-breach-pin-description")}</p>
);
recommendationSteps = pinRecommendationSteps;
breachIllustration = pinIllustration;
break;
default:
title = l10n.getString("high-risk-breach-none-title");
secondaryDescription = (
<>
<p>
{l10n.getString("high-risk-breach-none-description", {
// TODO: Expose email list & count here https://mozilla-hub.atlassian.net/browse/MNTOR-2112
email_list: emailsFormatter.format(emailsMonitored),
})}
</p>
<p>
{l10n.getString("high-risk-breach-none-sub-description-part-one")}
</p>
<ul>
<li>
{l10n.getString("high-risk-breach-none-sub-description-ssn")}
</li>
<li>
{l10n.getString(
"high-risk-breach-none-sub-description-bank-account"
)}
</li>
<li>
{l10n.getString(
"high-risk-breach-none-sub-description-cc-number"
)}
</li>
<li>
{l10n.getString("high-risk-breach-none-sub-description-pin")}
</li>
</ul>
</>
);
breachIllustration = NoBreachesIllustration;
break;
}
return {
title,
secondaryDescription,
recommendationSteps,
breachIllustration,
};
};

Просмотреть файл

@ -1,31 +0,0 @@
@import "../../../../../../../tokens";
.recommendations {
padding-top: $spacing-md;
p {
font: $text-body-sm;
}
ol {
padding-top: $spacing-md;
list-style-position: inside;
padding-left: 0;
}
}
.breachItemsWrapper {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
.breachItem {
font: $text-body-sm;
font-weight: 600;
background: $color-grey-10;
padding: $spacing-xs $spacing-sm;
border-radius: $border-radius-md;
.date {
font-weight: 400;
}
}
}

Просмотреть файл

@ -1,57 +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 { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import { createRandomBreach } from "../../../../../../../../apiMocks/mockData";
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
const meta: Meta<typeof HighRiskBreachLayout> = {
title: "ResolutionLayout",
component: HighRiskBreachLayout,
};
export default meta;
type Story = StoryObj<typeof HighRiskBreachLayout>;
const scannedResultsArraySample: SubscriberBreach[] = Array.from(
{ length: 5 },
() => createRandomBreach({ isHighRiskOnly: true })
);
export const CreditCard: Story = {
args: {
typeOfBreach: "creditCard",
breachData: getGuidedExperienceBreaches(scannedResultsArraySample),
},
};
export const BankAccount: Story = {
args: {
typeOfBreach: "bankAccount",
breachData: getGuidedExperienceBreaches(scannedResultsArraySample),
},
};
export const SSN: Story = {
args: {
typeOfBreach: "ssnBreaches",
breachData: getGuidedExperienceBreaches(scannedResultsArraySample),
},
};
export const PIN: Story = {
args: {
typeOfBreach: "pin",
breachData: getGuidedExperienceBreaches(scannedResultsArraySample),
},
};
export const None: Story = {
args: {
typeOfBreach: "none",
breachData: getGuidedExperienceBreaches(scannedResultsArraySample),
},
};

Просмотреть файл

@ -0,0 +1,110 @@
/* 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 { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import { createRandomBreach } from "../../../../../../../../apiMocks/mockData";
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
import creditCardIllustration from "../images/high-risk-data-breach-credit-card.svg";
import socialSecurityNumberIllustration from "../images/high-risk-data-breach-ssn.svg";
import bankAccountIllustration from "../images/high-risk-data-breach-bank-account.svg";
import pinIllustration from "../images/high-risk-data-breach-pin.svg";
import noBreachesIllustration from "../images/high-risk-breaches-none.svg";
const meta: Meta<typeof HighRiskBreachLayout> = {
title: "ResolutionLayout",
component: HighRiskBreachLayout,
};
export default meta;
type Story = StoryObj<typeof HighRiskBreachLayout>;
const scannedResultsArraySample: SubscriberBreach[] = Array.from(
{ length: 5 },
() => createRandomBreach({ isHighRiskOnly: true })
);
const summaryString = "It appeared in 2 data breaches:";
const recommendations = {
title: "Heres what to do",
steps: (
<ol>
<li>Recommendation one</li>
<li>Recommendation two</li>
<li>Recommendation three</li>
</ol>
),
};
const content = {
summary: summaryString,
description: <p>Security recommendation description text.</p>,
recommendations,
};
const guidedExperienceBreaches = getGuidedExperienceBreaches(
scannedResultsArraySample,
["test@mozilla.com"]
);
export const CreditCard: Story = {
args: {
pageData: {
type: "credit-card",
title: "Credit card",
illustration: creditCardIllustration,
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
content,
},
},
};
export const BankAccount: Story = {
args: {
pageData: {
type: "bank-account",
title: "Bank account",
illustration: bankAccountIllustration,
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
content,
},
},
};
export const SSN: Story = {
args: {
pageData: {
type: "ssn",
title: "SNN",
illustration: socialSecurityNumberIllustration,
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
content,
},
},
};
export const PIN: Story = {
args: {
pageData: {
type: "pin",
title: "PIN",
illustration: pinIllustration,
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
content,
},
},
};
export const None: Story = {
args: {
pageData: {
type: "none",
title: "No breaches",
illustration: noBreachesIllustration,
exposedData: [],
content,
},
},
};

Просмотреть файл

@ -14,31 +14,31 @@ import Meta, {
None,
} from "./HighRiskBreachLayout.stories";
it("high risk data breach component passes the axe accessiblity test suite", async () => {
it("high risk data breach component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(CreditCard, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("bank account data breach component passes the axe accessiblity test suite", async () => {
it("bank account data breach component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(BankAccount, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("SSN data breach component passes the axe accessiblity test suite", async () => {
it("SSN data breach component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(SSN, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("PIN data breach component passes the axe accessiblity test suite", async () => {
it("PIN data breach component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(PIN, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("Zero state breach component passes the axe accessiblity test suite", async () => {
it("Zero state breach component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(None, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();

Просмотреть файл

@ -4,112 +4,64 @@
"use client";
import styles from "./HighRiskBreachLayout.module.scss";
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import Link from "next/link";
import { ResolutionContainer } from "../ResolutionContainer";
import { ResolutionContent } from "../ResolutionContent";
import { Button } from "../../../../../../../components/server/Button";
import { useL10n } from "../../../../../../../hooks/l10n";
import { ResolutionContentLayout } from "../ResolutionContentLayout";
import { HighRiskBreachContent } from "./HighRiskBreachContent";
import { HighRiskBreach } from "./highRiskBreachData";
type HighRiskBreachLayoutProps = {
typeOfBreach: "creditCard" | "ssnBreaches" | "bankAccount" | "pin" | "none";
breachData: GuidedExperienceBreaches;
export type HighRiskBreachLayoutProps = {
locale: string;
pageData: HighRiskBreach;
};
export const HighRiskBreachLayout = (props: HighRiskBreachLayoutProps) => {
export function HighRiskBreachLayout({
pageData,
locale,
}: HighRiskBreachLayoutProps) {
const l10n = useL10n();
const highRiskDataBreaches = props.breachData.highRisk;
let exposedData: SubscriberBreach[] = [];
const { title, illustration, content, exposedData, type } = pageData;
const hasBreaches = type !== "none";
if (props.breachData) {
switch (props.typeOfBreach) {
case "ssnBreaches":
exposedData = highRiskDataBreaches.ssnBreaches;
break;
case "creditCard":
exposedData = highRiskDataBreaches.creditCardBreaches;
break;
case "bankAccount":
exposedData = highRiskDataBreaches.bankBreaches;
break;
case "pin":
exposedData = highRiskDataBreaches.pinBreaches;
break;
case "none":
default:
exposedData = [];
break;
}
}
const dateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "short",
});
const listOfBreaches = exposedData.map((item: SubscriberBreach) => (
<div key={item.id} className={styles.breachItem}>
{l10n.getFragment("high-risk-breach-name-and-date", {
elems: { breach_date: <span className={styles.date} /> },
vars: {
breach_name: item.title,
breach_date: dateFormatter.format(new Date(item.addedDate)),
},
})}
</div>
));
const breachList = (
<div className={styles.breachItemsWrapper}>{listOfBreaches}</div>
);
const primaryCta =
props.typeOfBreach !== "none"
? {
label: l10n.getString("high-risk-breach-mark-as-fixed"),
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore start */
onClick: () => {
// TODO: MNTOR-1700 Add routing logic + fix event here
},
skip: "/", // TODO: MNTOR-1700 Add routing logic here
}
: {
label: l10n.getString("high-risk-breach-none-continue"),
onClick: () => {
// TODO: MNTOR-1700 Add routing logic
},
/* c8 ignore stop */
};
const highRiskBreachContent = HighRiskBreachContent({
locale: props.locale,
typeOfBreach: props.typeOfBreach,
});
return (
<ResolutionContentLayout
type="highRisk"
title={highRiskBreachContent.title}
illustration={highRiskBreachContent.breachIllustration}
cta={primaryCta}
estimatedTime={props.typeOfBreach !== "none" ? 15 : undefined}
>
{props.typeOfBreach !== "none" && (
<ResolutionContainer
type="securityRecommendations"
title={title}
illustration={illustration}
cta={
<>
<p>
{l10n.getString("high-risk-breach-summary", {
num_breaches: exposedData.length,
})}
</p>
{breachList}
{highRiskBreachContent.secondaryDescription}
<div className={styles.recommendations}>
<h4>{l10n.getString("high-risk-breach-heading")}</h4>
<p>{l10n.getString("high-risk-breach-subheading")}</p>
{highRiskBreachContent.recommendationSteps}
</div>
<Button
variant="primary"
small
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore next 3 */
onClick={() => {
// TODO: MNTOR-1700 Add routing logic + fix event here
}}
>
{hasBreaches
? l10n.getString("high-risk-breach-mark-as-fixed")
: l10n.getString("high-risk-breach-none-continue")}
</Button>
{hasBreaches && (
<Link
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore next */
href="/"
>
{l10n.getString("high-risk-breach-skip")}
</Link>
)}
</>
)}
</ResolutionContentLayout>
}
estimatedTime={hasBreaches ? 15 : undefined}
>
<ResolutionContent
content={content}
exposedData={exposedData}
locale={locale}
/>
</ResolutionContainer>
);
};
}

Просмотреть файл

@ -1,42 +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 { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
import Link from "next/link";
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
import { getLocale } from "../../../../../../../functions/server/l10n";
type Props = {
breaches: GuidedExperienceBreaches;
};
export const View = (props: Props) => {
const locale = getLocale();
return (
<div>
{/* TODO: MNTOR-1700 Add routing logic here, currently default to no high risk breach data */}
<HighRiskBreachLayout
typeOfBreach="none"
breachData={props.breaches}
locale={locale}
/>
{/* TODO: Remove all bottom links */}
<Link href="/redesign/user/dashboard/fix/high-risk-data-breaches/social-security-number">
SSN Breaches
</Link>
<br />
<Link href="/redesign/user/dashboard/fix/high-risk-data-breaches/credit-card-number">
Credit card
</Link>
<br />
<Link href="/redesign/user/dashboard/fix/high-risk-data-breaches/bank-account">
Bank Account
</Link>
<br />
<Link href="/redesign/user/dashboard/fix/high-risk-data-breaches/pin-number">
Pin Number
</Link>
</div>
);
};

Просмотреть файл

@ -2,28 +2,47 @@
* 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 { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { redirect } from "next/navigation";
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { getLocale } from "../../../../../../../../functions/server/l10n";
import { getHighRiskBreachesByType } from "../highRiskBreachData";
export default async function SocialSecurityNumberDataBreach() {
const locale = getLocale();
interface SecurityRecommendationsProps {
params: {
type: string;
};
}
export default async function SecurityRecommendations({
params,
}: SecurityRecommendationsProps) {
const session = await getServerSession(authOptions);
const locale = getLocale();
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const guidedExperience = getGuidedExperienceBreaches(breaches);
return (
<HighRiskBreachLayout
breachData={guidedExperience}
typeOfBreach="ssnBreaches"
locale={locale}
/>
const subscriberEmails = await getSubscriberEmails(session.user);
const guidedExperienceBreaches = getGuidedExperienceBreaches(
breaches,
subscriberEmails
);
const { type } = params;
const pageData = getHighRiskBreachesByType({
dataType: type,
breaches: guidedExperienceBreaches,
locale,
});
if (!pageData) {
redirect("/redesign/user/dashboard");
}
return <HighRiskBreachLayout pageData={pageData} locale={locale} />;
}

Просмотреть файл

@ -1,29 +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 { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { redirect } from "next/navigation";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { getLocale } from "../../../../../../../../functions/server/l10n";
export default async function BankAccountDataBreach() {
const locale = getLocale();
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const guidedExperience = getGuidedExperienceBreaches(breaches);
return (
<HighRiskBreachLayout
typeOfBreach="bankAccount"
breachData={guidedExperience}
locale={locale}
/>
);
}

Просмотреть файл

@ -1,29 +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 { getServerSession } from "next-auth";
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { redirect } from "next/navigation";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { getLocale } from "../../../../../../../../functions/server/l10n";
export default async function CreditCardDataBreach() {
const locale = getLocale();
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const guidedExperience = getGuidedExperienceBreaches(breaches);
return (
<HighRiskBreachLayout
typeOfBreach="creditCard"
breachData={guidedExperience}
locale={locale}
/>
);
}

Просмотреть файл

@ -0,0 +1,238 @@
/* 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 { ReactNode } from "react";
import { getL10n } from "../../../../../../../functions/server/l10n";
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import creditCardIllustration from "../images/high-risk-data-breach-credit-card.svg";
import socialSecurityNumberIllustration from "../images/high-risk-data-breach-ssn.svg";
import bankAccountIllustration from "../images/high-risk-data-breach-bank-account.svg";
import pinIllustration from "../images/high-risk-data-breach-pin.svg";
import noBreachesIllustration from "../images/high-risk-breaches-none.svg";
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { FraudAlertModal } from "./FraudAlertModal";
export type HighRiskBreachContent = {
summary: string;
description: ReactNode;
recommendations?: {
title: string;
steps: ReactNode;
subtitle?: string;
};
};
export type HighRiskBreachTypes =
| "credit-card"
| "ssn"
| "bank-account"
| "pin"
| "none";
export type HighRiskBreach = {
type: HighRiskBreachTypes;
title: string;
illustration: string;
content: HighRiskBreachContent;
exposedData: SubscriberBreach[];
};
function getHighRiskBreachesByType({
dataType,
breaches,
locale,
}: {
dataType: string;
breaches: GuidedExperienceBreaches;
locale: string;
}) {
const l10n = getL10n();
// TODO: Expose email list & count here https://mozilla-hub.atlassian.net/browse/MNTOR-2112
const emailsFormatter = new Intl.ListFormat(locale, {
style: "long",
type: "conjunction",
});
const highRiskBreachData: HighRiskBreach[] = [
{
type: "credit-card",
title: l10n.getString("high-risk-breach-credit-card-title"),
illustration: creditCardIllustration,
exposedData: breaches.highRisk.creditCardBreaches,
content: {
summary: l10n.getString("high-risk-breach-summary", {
num_breaches: 0,
}),
description: (
<p>{l10n.getString("high-risk-breach-credit-card-description")}</p>
),
recommendations: {
title: l10n.getString("high-risk-breach-heading"),
subtitle: l10n.getString("high-risk-breach-subheading"),
steps: (
<ol>
<li>{l10n.getString("high-risk-breach-credit-card-step-one")}</li>
<li>{l10n.getString("high-risk-breach-credit-card-step-two")}</li>
<li>
{l10n.getString("high-risk-breach-credit-card-step-three")}
</li>
</ol>
),
},
},
},
{
type: "ssn",
title: l10n.getString("high-risk-breach-social-security-title"),
illustration: socialSecurityNumberIllustration,
exposedData: breaches.highRisk.ssnBreaches,
content: {
summary: l10n.getString("high-risk-breach-summary", {
num_breaches: 0,
}),
description: (
<p>
{l10n.getString("high-risk-breach-social-security-description")}
</p>
),
recommendations: {
title: l10n.getString("high-risk-breach-heading"),
subtitle: l10n.getString("high-risk-breach-subheading"),
steps: (
<ol>
<li>
{l10n.getFragment("high-risk-breach-social-security-step-one", {
elems: {
link_to_info: (
<a
href="https://consumer.ftc.gov/articles/what-know-about-credit-freezes-fraud-alerts"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
<FraudAlertModal />
</li>
<li>
{l10n.getFragment("high-risk-breach-social-security-step-two", {
elems: {
link_to_info: (
<a
href="https://www.annualcreditreport.com/index.action"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
</ol>
),
},
},
},
{
type: "bank-account",
title: l10n.getString("high-risk-breach-bank-account-title"),
illustration: bankAccountIllustration,
exposedData: breaches.highRisk.bankBreaches,
content: {
summary: l10n.getString("high-risk-breach-summary", {
num_breaches: 0,
}),
description: (
<p>{l10n.getString("high-risk-breach-bank-account-description")}</p>
),
recommendations: {
title: l10n.getString("high-risk-breach-heading"),
subtitle: l10n.getString("high-risk-breach-subheading"),
steps: (
<ol>
<li>
{l10n.getString("high-risk-breach-bank-account-step-one")}
</li>
<li>
{l10n.getString("high-risk-breach-bank-account-step-two")}
</li>
<li>
{l10n.getString("high-risk-breach-bank-account-step-three")}
</li>
</ol>
),
},
},
},
{
type: "pin",
title: l10n.getString("high-risk-breach-pin-title"),
illustration: pinIllustration,
exposedData: breaches.highRisk.pinBreaches,
content: {
summary: l10n.getString("high-risk-breach-summary", {
num_breaches: 0,
}),
description: (
<p>{l10n.getString("high-risk-breach-pin-description")}</p>
),
recommendations: {
title: l10n.getString("high-risk-breach-heading"),
subtitle: l10n.getString("high-risk-breach-subheading"),
steps: (
<ol>
<li>{l10n.getString("high-risk-breach-pin-step-one")}</li>
<li>{l10n.getString("high-risk-breach-pin-step-two")}</li>
<li>{l10n.getString("high-risk-breach-pin-step-three")}</li>
</ol>
),
},
},
},
{
type: "none",
title: l10n.getString("high-risk-breach-none-title"),
illustration: noBreachesIllustration,
exposedData: [],
content: {
summary: "",
description: (
<>
<p>
{l10n.getString("high-risk-breach-none-description", {
// TODO: Expose email list & count here https://mozilla-hub.atlassian.net/browse/MNTOR-2112
email_list: emailsFormatter.format(breaches.emails),
})}
</p>
<p>
{l10n.getString("high-risk-breach-none-sub-description-part-one")}
</p>
<ul>
<li>
{l10n.getString("high-risk-breach-none-sub-description-ssn")}
</li>
<li>
{l10n.getString(
"high-risk-breach-none-sub-description-bank-account"
)}
</li>
<li>
{l10n.getString(
"high-risk-breach-none-sub-description-cc-number"
)}
</li>
<li>
{l10n.getString("high-risk-breach-none-sub-description-pin")}
</li>
</ul>
</>
),
},
},
];
return highRiskBreachData.find((content) => content.type === dataType);
}
export { getHighRiskBreachesByType };

Просмотреть файл

@ -3,19 +3,42 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { getServerSession } from "next-auth";
import { getSubscriberBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
import { authOptions } from "../../../../../../../api/utils/auth";
import { redirect } from "next/navigation";
import { View } from "./View";
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
import { authOptions } from "../../../../../../../api/utils/auth";
import { getSubscriberEmails } from "../../../../../../../functions/server/getSubscriberEmails";
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
import { getLocale } from "../../../../../../../functions/server/l10n";
import { getSubscriberBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { getHighRiskBreachesByType } from "./highRiskBreachData";
export default async function HighRiskDataBreaches() {
const session = await getServerSession(authOptions);
const locale = getLocale();
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const guidedExperience = getGuidedExperienceBreaches(breaches);
const subscriberEmails = await getSubscriberEmails(session.user);
const guidedExperienceBreaches = getGuidedExperienceBreaches(
breaches,
subscriberEmails
);
return <View breaches={guidedExperience} />;
const pageData = getHighRiskBreachesByType({
dataType: "none",
breaches: guidedExperienceBreaches,
locale,
});
if (!pageData) {
redirect("/redesign/user/dashboard/fix/high-risk-data-breaches");
}
return (
<div>
{/* TODO: MNTOR-1700 Add routing logic here, currently default to no high risk breach data */}
<HighRiskBreachLayout pageData={pageData} locale={locale} />
</div>
);
}

Просмотреть файл

@ -1,29 +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 { getServerSession } from "next-auth";
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { redirect } from "next/navigation";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { getLocale } from "../../../../../../../../functions/server/l10n";
export default async function pinDataBreach() {
const locale = getLocale();
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const guidedExperience = getGuidedExperienceBreaches(breaches);
return (
<HighRiskBreachLayout
typeOfBreach="pin"
breachData={guidedExperience}
locale={locale}
/>
);
}

Просмотреть файл

@ -0,0 +1,39 @@
<svg width="242" height="195" viewBox="0 0 242 195" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.146 11.3187C137.923 9.46531 165.959 35.2181 184.266 61.6622C201.121 86.0101 206.292 116.966 196.418 144.989C187.567 170.106 161.997 179.045 137.394 188.389C116.376 196.371 94.6227 196.786 73.1628 190.125C44.9358 181.363 10.0173 175.572 1.98749 146.725C-6.19355 117.336 19.9038 92.6531 38.4431 68.6061C57.317 44.1252 75.5825 13.1013 106.146 11.3187Z" fill="#D9BFFF"/>
</g>
<path d="M141.597 85.519C141.374 83.3649 141.137 81.2108 140.937 78.755C140.405 72.2926 139.07 63.1089 138.101 57.2137C138.008 56.6824 137.943 56.1725 137.886 55.6627C137.584 53.2932 137.247 50.6005 134.656 48.6044C131.576 46.2348 126.961 46.0194 123.3 46.0912C118.756 46.1917 114.98 46.0912 111.326 46.0912C107.113 46.0122 102.756 45.9332 97.3721 46.0912C84.559 46.5292 74.6244 46.8882 63.3833 47.4483C57.5547 47.7427 46.9955 48.949 41.7985 51.0385C32.5099 54.8298 35.8263 78.8771 37.6854 89.0374C37.8936 90.1791 38.0587 91.4142 38.2453 92.7138C38.9631 98.092 39.8461 104.203 43.8802 107.47C48.4455 111.182 53.4559 111.778 59.3851 111.778H61.4237C64.5677 111.728 67.4893 111.663 70.2888 111.606C73.8348 111.527 77.1798 111.455 80.489 111.419C82.9798 111.419 85.5137 111.419 88.0189 111.419C93.1298 111.419 98.4129 111.419 103.466 111.211C107.228 111.067 110.587 111.053 113.839 111.038C116.488 111.038 124.972 110.406 132.15 109.846C138.317 109.365 142.946 104.007 142.53 97.8335C142.207 92.8933 141.834 87.7521 141.597 85.519Z" fill="#9059FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M53.5727 98.1624C55.5575 98.3542 57.5509 98.4446 59.545 98.4353C62.8921 98.4353 66.2508 98.2184 69.5499 98.0052C69.8521 97.9858 70.1543 97.9664 70.4558 97.947L70.6187 97.9355C74.337 97.6735 78.1831 97.402 81.8548 97.4588C82.2445 97.4753 82.6228 97.3209 82.8884 97.0351C83.1612 96.7608 83.3112 96.3874 83.3047 96.0011C83.276 93.8111 82.4721 85.2376 82.4362 84.8786C82.3694 84.1362 81.7456 83.5689 81.0005 83.5718C80.9604 83.5725 80.8369 83.5739 80.6438 83.5754C78.7258 83.5955 69.8909 83.6852 66.1991 83.9595L61.9281 84.3042C61.7336 84.3193 61.539 84.3343 61.3438 84.3501C57.1187 84.6812 52.7659 85.0222 48.4977 85.0222H48.3183C47.8976 85.0237 47.4985 85.2104 47.2272 85.532C46.9523 85.8552 46.8338 86.2838 46.9042 86.7025C47.0405 87.4844 47.131 88.3188 47.22 89.1309C47.225 89.1761 47.2293 89.2206 47.2344 89.2659C47.6363 92.9925 48.1388 97.6239 53.5727 98.1624ZM49.9922 88.071C49.9843 88.0079 49.9771 87.944 49.9692 87.8801C54.068 87.8154 58.1739 87.4851 62.165 87.1476C63.5626 87.0234 64.9788 86.9085 66.4145 86.803C69.537 86.5732 76.7295 86.4368 79.7013 86.4368L79.7207 86.6493C79.9044 88.7008 80.2303 92.3391 80.3689 94.5579C77.1164 94.5859 73.7965 94.8156 70.5721 95.0382C70.4666 95.0454 70.3603 95.0533 70.2548 95.0605L70.2232 95.0626C64.8065 95.4425 59.2119 95.8345 53.8598 95.3046C51.0244 95.0246 50.5507 93.2654 50.0913 88.9571C50.0618 88.6606 50.0274 88.369 49.9922 88.071Z" fill="#E7DFFF"/>
<path d="M91.6935 94.4807C91.5033 94.4814 91.3145 94.4419 91.1408 94.3658C90.789 94.2179 90.5105 93.9372 90.367 93.5846C90.2227 93.2313 90.2248 92.8357 90.3727 92.4846C91.5212 89.7344 94.2346 86.4745 96.7757 85.7996C97.5552 85.5403 98.4137 85.7278 99.0152 86.2878C99.2263 86.491 99.395 86.7352 99.5105 87.0059C100.839 85.8067 102.08 85.1892 103.229 85.1677C103.845 85.1253 104.441 85.3946 104.815 85.8857C105.44 86.7546 105.153 87.8244 104.765 88.9661L104.88 88.8225C105.705 87.8244 106.717 86.5679 108.311 86.6684C109.383 86.7948 110.353 87.3685 110.981 88.2481C111.234 88.1734 111.476 88.065 111.699 87.925C112.319 87.459 113.196 87.5695 113.682 88.1749C114.166 88.7802 114.083 89.6612 113.494 90.1653C112.819 90.6385 112.062 90.9824 111.261 91.1777L110.91 91.2926C110.296 91.4944 109.624 91.2625 109.266 90.7254C108.548 89.6411 108.167 89.5334 108.167 89.5334C107.778 89.8723 107.427 90.2529 107.119 90.6679C106.323 91.6301 105.332 92.822 103.832 93.0015C103.077 93.1394 102.306 92.8658 101.808 92.2835C101.054 91.3285 101.377 90.0791 101.808 88.8297C100.813 89.6993 99.9262 90.6859 99.166 91.7665C98.7662 92.3101 98.0412 92.5025 97.4246 92.2275C96.808 91.9532 96.4663 91.2854 96.6034 90.6248L96.6608 90.3807C96.78 89.8759 96.8711 89.3654 96.9336 88.8512C95.2051 90.0253 93.8563 91.6768 93.0502 93.6047C92.8205 94.1475 92.2821 94.4944 91.6935 94.4807Z" fill="#0083B3"/>
<path d="M140.519 74.5627C140.074 70.5273 139.407 65.58 138.639 60.6398C138.506 59.7667 137.748 59.1255 136.866 59.1391C132.099 59.2038 127.261 59.4335 122.581 59.6561C117.391 59.9003 112.022 60.1516 106.789 60.1803C90.4083 60.2808 76.9349 60.6901 63.1097 61.5159C54.9768 61.9969 42.3575 62.5858 37.3113 62.8155C36.3924 62.8522 35.6574 63.591 35.6244 64.5101C35.488 69.3612 35.7012 74.2159 36.2632 79.0362C36.3587 79.9351 37.1174 80.6173 38.0219 80.6158C38.0958 80.623 38.1705 80.623 38.2444 80.6158C47.1526 79.4885 56.6636 79.3018 65.8517 79.1151C69.7064 79.0361 73.6831 78.9572 77.5163 78.8064C85.2113 78.5048 93.0571 78.2535 100.637 78.0093C113.17 77.6072 126.127 77.1908 138.875 76.5732C139.362 76.5467 139.817 76.3205 140.132 75.9486C140.453 75.5637 140.594 75.0589 140.519 74.5627Z" fill="#432F71"/>
<path d="M205.882 120.725C205.635 118.341 205.373 115.958 205.151 113.241C204.563 106.091 203.086 95.9296 202.014 89.4071C201.911 88.8192 201.839 88.2551 201.776 87.691C201.442 85.0693 201.069 82.0901 198.202 79.8815C194.794 77.2598 189.688 77.0215 185.637 77.1009C180.61 77.2121 176.432 77.1009 172.39 77.1009C167.728 77.0135 162.907 76.9261 156.95 77.1009C142.774 77.5855 131.782 77.9827 119.345 78.6024C112.896 78.9281 101.213 80.2628 95.4628 82.5747C85.1858 86.7694 88.855 113.376 90.912 124.617C91.1423 125.881 91.325 127.247 91.5315 128.685C92.3257 134.636 93.3026 141.396 97.766 145.011C102.817 149.118 108.361 149.778 114.921 149.778H117.176C120.655 149.722 123.888 149.651 126.985 149.587C130.908 149.5 134.609 149.42 138.271 149.381C141.027 149.381 143.83 149.381 146.602 149.381C152.257 149.381 158.102 149.381 163.693 149.15C167.855 148.991 171.572 148.975 175.17 148.96C178.1 148.96 187.488 148.26 195.43 147.641C202.253 147.108 207.375 141.179 206.914 134.35C206.557 128.884 206.144 123.195 205.882 120.725Z" fill="#C9EFFD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.49 134.714C110.686 134.926 112.891 135.026 115.097 135.015C118.801 135.015 122.517 134.776 126.167 134.54C126.501 134.518 126.836 134.497 127.169 134.475L127.35 134.463C131.464 134.173 135.719 133.872 139.781 133.935C140.213 133.953 140.631 133.782 140.925 133.466C141.227 133.163 141.393 132.75 141.386 132.322C141.354 129.899 140.464 120.413 140.425 120.016C140.351 119.195 139.661 118.567 138.836 118.57C138.792 118.571 138.655 118.573 138.442 118.574C136.319 118.596 126.544 118.696 122.46 118.999L117.734 119.381C117.519 119.397 117.304 119.414 117.088 119.431C112.413 119.798 107.597 120.175 102.874 120.175H102.676C102.211 120.177 101.769 120.383 101.469 120.739C101.165 121.097 101.034 121.571 101.111 122.034C101.262 122.899 101.362 123.822 101.461 124.721C101.466 124.771 101.471 124.82 101.477 124.87C101.921 128.993 102.477 134.118 108.49 134.714ZM104.528 123.548C104.519 123.478 104.511 123.408 104.503 123.337C109.038 123.265 113.58 122.9 117.996 122.527C119.543 122.389 121.11 122.262 122.698 122.145C126.153 121.891 134.111 121.74 137.399 121.74L137.42 121.975C137.623 124.245 137.984 128.271 138.137 130.725C134.539 130.756 130.866 131.011 127.298 131.257C127.181 131.265 127.064 131.274 126.947 131.282L126.912 131.284C120.919 131.704 114.729 132.138 108.807 131.552C105.67 131.242 105.146 129.295 104.638 124.529C104.605 124.201 104.567 123.878 104.528 123.548Z" fill="#0083B3"/>
<path d="M150.667 130.64C150.457 130.641 150.248 130.597 150.056 130.513C149.667 130.349 149.358 130.039 149.2 129.649C149.04 129.258 149.042 128.82 149.206 128.431C150.477 125.389 153.479 121.782 156.29 121.035C157.153 120.748 158.103 120.956 158.768 121.575C159.002 121.8 159.188 122.07 159.316 122.37C160.786 121.043 162.16 120.36 163.43 120.336C164.112 120.289 164.771 120.587 165.185 121.13C165.876 122.092 165.559 123.275 165.13 124.539L165.257 124.38C166.17 123.275 167.29 121.885 169.053 121.996C170.24 122.136 171.313 122.771 172.008 123.744C172.287 123.661 172.555 123.541 172.802 123.387C173.487 122.871 174.459 122.993 174.996 123.663C175.532 124.333 175.439 125.308 174.787 125.865C174.041 126.389 173.203 126.769 172.317 126.985L171.928 127.113C171.25 127.336 170.506 127.079 170.11 126.485C169.315 125.285 168.894 125.166 168.894 125.166C168.464 125.541 168.075 125.962 167.735 126.421C166.853 127.486 165.757 128.805 164.097 129.003C163.263 129.156 162.409 128.853 161.858 128.209C161.024 127.152 161.381 125.77 161.858 124.388C160.757 125.35 159.776 126.441 158.935 127.637C158.493 128.238 157.69 128.451 157.008 128.147C156.326 127.843 155.948 127.105 156.1 126.374L156.163 126.104C156.295 125.545 156.396 124.98 156.465 124.411C154.553 125.71 153.06 127.538 152.168 129.671C151.914 130.271 151.319 130.655 150.667 130.64Z" fill="#0083B3"/>
<path d="M204.689 108.602C204.197 104.137 203.458 98.6635 202.608 93.1976C202.461 92.2316 201.624 91.5221 200.647 91.5372C195.373 91.6087 190.02 91.863 184.842 92.1092C179.1 92.3793 173.159 92.6574 167.369 92.6892C149.246 92.8004 134.338 93.2533 119.042 94.1669C110.043 94.6992 96.0812 95.3506 90.4979 95.6049C89.4814 95.6454 88.6681 96.4629 88.6316 97.4798C88.4807 102.847 88.7165 108.218 89.3384 113.552C89.444 114.546 90.2835 115.301 91.2842 115.299C91.366 115.307 91.4486 115.307 91.5304 115.299C101.387 114.052 111.91 113.846 122.076 113.639C126.341 113.552 130.74 113.464 134.982 113.297C143.495 112.964 152.176 112.686 160.563 112.416C174.43 111.971 188.765 111.51 202.87 110.827C203.409 110.797 203.912 110.547 204.26 110.135C204.616 109.71 204.772 109.151 204.689 108.602Z" fill="#432F71"/>
<path d="M97.4694 35.6225C97.5479 37.6207 97.4727 39.0061 97.4694 42.5664C97.4686 42.8778 97.4694 42.2546 97.4694 42.5664C97.4678 45.4011 97.4039 47.6924 97.4694 49.5103C97.5018 50.4132 98.3122 51.2791 99.2054 51.2463C100.099 51.2135 100.974 50.4132 100.941 49.5103C100.878 47.7433 100.94 45.3634 100.941 42.5664C100.941 42.8777 100.941 42.2555 100.941 42.5664C100.945 38.9623 101.021 37.6792 100.941 35.6225C100.906 34.7197 100.098 33.8508 99.2054 33.8865C98.3122 33.9222 97.4338 34.7197 97.4694 35.6225Z" fill="#FCD400"/>
<path d="M93.9999 44.3024C94.8889 44.3493 96.1619 44.2668 97.4718 44.3024C97.1672 44.2943 97.64 44.3069 97.4718 44.3024C100.167 44.3736 101.254 44.1773 102.68 44.3024C103.532 44.3772 104.339 43.4001 104.416 42.5664C104.492 41.7327 103.533 40.9052 102.68 40.8304C101.168 40.6977 100.232 40.9034 97.4718 40.8304C97.168 40.8224 97.64 40.8349 97.4718 40.8304C96.1875 40.7955 94.8556 40.8756 93.9999 40.8304C93.1449 40.7853 92.3096 41.7305 92.2639 42.5664C92.2174 43.4022 93.1449 44.2573 93.9999 44.3024Z" fill="#FCD400"/>
<path d="M199.895 117.214C201.081 117.165 201.227 117.166 203.367 117.214C203.702 117.222 204.915 117.21 205.103 117.214C206.8 117.249 207.406 117.268 208.575 117.214C209.465 117.171 210.352 116.394 210.311 115.478C210.27 114.562 209.464 113.7 208.575 113.742C207.493 113.793 206.735 113.776 205.103 113.742C204.917 113.738 203.702 113.75 203.367 113.742C201.161 113.693 201.17 113.689 199.895 113.742C199.005 113.778 198.123 114.561 198.159 115.478C198.194 116.394 199.004 117.251 199.895 117.214Z" fill="#EAF3F3"/>
<path d="M205.101 120.686C205.119 119.865 205.094 118.385 205.101 117.214C205.099 117.574 205.101 117.013 205.101 117.214C205.114 114.494 205.031 113.447 205.101 112.006C205.143 111.119 204.273 110.312 203.365 110.27C202.455 110.228 201.671 111.118 201.629 112.006C201.556 113.508 201.642 114.448 201.629 117.214C201.627 117.574 201.629 117.014 201.629 117.214C201.622 118.369 201.646 119.886 201.629 120.686C201.609 121.573 202.455 122.403 203.365 122.422C204.274 122.441 205.081 121.573 205.101 120.686Z" fill="#EAF3F3"/>
<path d="M187.74 16.5266C187.784 17.4677 187.648 18.548 187.74 19.9986C187.727 19.794 187.754 20.2034 187.74 19.9986C187.965 23.5404 187.74 24.9582 187.74 26.9425C187.74 27.7765 188.697 28.6785 189.476 28.6785C190.256 28.6785 191.212 27.7765 191.212 26.9425C191.212 24.8675 191.441 23.608 191.212 19.9986C191.199 19.7941 191.226 20.2028 191.212 19.9986C191.122 18.5646 191.255 17.4454 191.212 16.5266C191.173 15.6936 190.254 14.7489 189.476 14.7906C188.698 14.8323 187.701 15.6937 187.74 16.5266Z" fill="#90BFFF"/>
<path d="M186.006 23.4705C187.652 23.646 188.297 23.6425 191.214 23.4705C191.897 23.4302 192.569 23.4891 192.95 23.4705C193.686 23.4344 194.104 23.4705 194.686 23.4705C195.557 23.4705 196.422 22.5775 196.422 21.7345C196.422 20.8915 195.557 19.9985 194.686 19.9985C194.044 19.9985 193.734 19.9601 192.95 19.9985C192.555 20.0178 191.909 19.9576 191.214 19.9985C188.478 20.1598 187.412 20.1486 186.006 19.9985C185.14 19.9062 184.365 20.8966 184.27 21.7345C184.174 22.5724 185.14 23.3782 186.006 23.4705Z" fill="#90BFFF"/>
<path d="M47.1262 158.877C47.1699 159.818 47.0338 160.899 47.1262 162.349C47.1135 162.145 47.1389 162.555 47.1262 162.349C47.3513 165.891 47.1262 167.309 47.1262 169.293C47.1262 170.127 48.0832 171.029 48.8622 171.029C49.6418 171.029 50.5982 170.127 50.5982 169.293C50.5982 167.218 50.8275 165.959 50.5982 162.349C50.5855 162.145 50.6116 162.553 50.5982 162.349C50.5078 160.915 50.6412 159.796 50.5982 158.877C50.5593 158.044 49.6411 157.1 48.8622 157.141C48.0839 157.184 47.0874 158.044 47.1262 158.877Z" fill="#90BFFF"/>
<path d="M45.3905 165.821C47.0356 165.996 47.682 165.993 50.5984 165.821C50.9998 165.797 52.1096 165.833 52.3344 165.821C53.3597 165.764 53.348 165.821 54.0704 165.821C54.9411 165.821 55.8064 164.926 55.8064 164.085C55.8064 163.244 54.9411 162.349 54.0704 162.349C53.2762 162.349 53.4141 162.289 52.3344 162.349C52.1049 162.361 51.0038 162.325 50.5984 162.349C47.8626 162.51 46.7966 162.499 45.3905 162.349C44.5245 162.257 43.7499 163.249 43.6545 164.085C43.5591 164.921 44.5245 165.729 45.3905 165.821Z" fill="#90BFFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M205.1 79.0219C205.213 80.8096 206.684 80.806 206.836 79.0219C207.19 74.8789 209.289 72.3323 212.044 72.0779C211.813 72.0932 212.264 72.0743 212.044 72.0779C213.874 71.9526 213.878 70.342 212.044 70.342C211.801 70.342 212.278 70.3238 212.044 70.342C209.114 70.3659 207.495 70.3671 206.836 66.87C206.805 66.708 206.914 65.5611 206.836 65.134C206.763 64.7396 206.885 65.3811 206.836 65.134C206.502 63.4668 205.188 63.4349 205.1 65.134C205.087 65.3847 205.113 64.7 205.1 65.134C205.083 65.7193 205.131 66.5721 205.1 66.87C205.025 67.5919 205.479 67.931 205.1 68.606C204.432 69.7944 201.891 70.3753 199.892 70.342C198.064 70.3122 198.071 71.9113 199.892 72.0779C202.255 72.2946 203.092 73.2827 203.364 73.8139C204.199 75.4698 204.981 77.1529 205.1 79.0219ZM212.044 70.342C211.846 70.3557 212.228 70.3398 212.044 70.342C211.874 70.2746 212.262 70.3391 212.044 70.342ZM212.044 72.0779C211.86 72.0779 212.217 72.0634 212.044 72.0779C211.89 72.1519 212.246 72.0613 212.044 72.0779ZM205.1 72.0779C205.689 71.654 204.7 70.9543 205.1 70.342C205.53 71.1239 206.172 71.6062 206.836 72.0779C206.242 72.6207 205.525 73.0545 205.1 73.8139C204.962 73.4914 205.258 72.3917 205.1 72.0779C205.596 72.4446 204.832 71.5562 205.1 72.0779Z" fill="#FCD400"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.7658 115.478C31.597 115.354 31.6013 113.742 29.7658 113.742C29.5269 113.742 29.9962 113.724 29.7658 113.742C26.8174 113.766 25.22 113.796 24.5579 110.27C24.5287 110.115 24.6318 108.938 24.5579 108.534C24.4853 108.137 24.6069 108.781 24.5579 108.534C24.2251 106.866 22.9101 106.834 22.8219 108.534C22.8091 108.784 22.8347 108.102 22.8219 108.534C22.8055 109.108 22.8511 109.977 22.8219 110.27C22.7486 111.004 23.2031 111.325 22.8219 112.006C22.152 113.194 19.6066 113.782 17.6139 113.742C15.7856 113.706 15.7934 115.307 17.6139 115.478C19.9807 115.701 20.8135 116.683 21.0859 117.214C21.918 118.87 22.701 120.552 22.8219 122.422C22.9385 124.208 24.405 124.204 24.5579 122.422C24.912 118.274 27.0059 115.728 29.7658 115.478C29.982 115.474 29.539 115.493 29.7658 115.478ZM29.7658 113.742C29.5703 113.755 29.9472 113.739 29.7658 113.742C29.598 113.676 29.9806 113.739 29.7658 113.742ZM29.7658 115.478C29.5852 115.478 29.9358 115.464 29.7658 115.478C29.6129 115.549 29.9642 115.462 29.7658 115.478ZM22.8219 115.478C23.4107 115.054 22.4215 114.355 22.8219 113.742C23.2507 114.524 23.8937 115.007 24.5579 115.478C23.9641 116.021 23.2479 116.453 22.8219 117.214C22.6846 116.891 22.9798 115.791 22.8219 115.478C23.3183 115.845 22.5538 114.954 22.8219 115.478Z" fill="#EAF3F3"/>
<path d="M123.511 21.7345C123.511 20.8991 122.734 19.9985 121.775 19.9985C120.816 19.9985 120.039 20.8991 120.039 21.7345C120.039 22.5701 120.816 23.4705 121.775 23.4705C122.734 23.4705 123.511 22.5701 123.511 21.7345Z" fill="#90BFFF"/>
<path d="M236.349 99.8537C236.349 99.0178 235.572 98.1177 234.613 98.1177C233.654 98.1177 232.877 99.0178 232.877 99.8537C232.877 100.69 233.654 101.59 234.613 101.59C235.572 101.59 236.349 100.69 236.349 99.8537Z" fill="#90BFFF"/>
<path d="M231.141 44.3024C231.141 43.4651 230.364 42.5664 229.405 42.5664C228.446 42.5664 227.669 43.4651 227.669 44.3024C227.669 45.1398 228.446 46.0384 229.405 46.0384C230.364 46.0384 231.141 45.1398 231.141 44.3024Z" fill="#EAF3F3"/>
<path d="M241.557 58.1901C241.557 57.3545 240.78 56.4541 239.821 56.4541C238.862 56.4541 238.085 57.3545 238.085 58.1901C238.085 59.0256 238.862 59.9261 239.821 59.9261C240.78 59.9261 241.557 59.0256 241.557 58.1901Z" fill="#4E6AF0"/>
<path d="M220.725 9.58272C220.725 8.74718 219.949 7.84674 218.989 7.84674C218.03 7.84674 217.253 8.74718 217.253 9.58272C217.253 10.4183 218.03 11.3187 218.989 11.3187C219.949 11.3187 220.725 10.4183 220.725 9.58272Z" fill="#FCD400"/>
<path d="M14.1434 132.838C14.1434 132 13.3656 131.102 12.4074 131.102C11.4482 131.102 10.6714 132 10.6714 132.838C10.6714 133.675 11.4482 134.574 12.4074 134.574C13.3656 134.574 14.1434 133.675 14.1434 132.838Z" fill="#FCD400"/>
<path d="M66.2225 47.7746C66.2225 46.8806 65.4456 46.0386 64.4865 46.0386C63.5282 46.0386 62.7505 46.8806 62.7505 47.7746C62.7505 48.6685 63.5282 49.5105 64.4865 49.5105C65.4456 49.5105 66.2225 48.6685 66.2225 47.7746Z" fill="#EAF3F3"/>
<path d="M76.6385 11.3185C76.6385 10.4266 75.8616 9.58252 74.9025 9.58252C73.9442 9.58252 73.1665 10.4266 73.1665 11.3185C73.1665 12.2104 73.9442 13.0545 74.9025 13.0545C75.8616 13.0545 76.6385 12.2104 76.6385 11.3185Z" fill="#4E6AF0"/>
<path d="M170.382 25.2066C170.382 24.4207 169.605 23.4706 168.646 23.4706C167.687 23.4706 166.91 24.4207 166.91 25.2066C166.91 25.9925 167.687 26.9425 168.646 26.9425C169.605 26.9425 170.382 25.9925 170.382 25.2066Z" fill="#EAF3F3"/>
<path d="M153.022 2.63881C153.022 1.80153 152.245 0.902832 151.286 0.902832C150.328 0.902832 149.55 1.80153 149.55 2.63881C149.55 3.47618 150.328 4.3748 151.286 4.3748C152.245 4.3748 153.022 3.47618 153.022 2.63881Z" fill="#FCD400"/>
<path d="M222.462 157.141C222.462 156.305 221.685 155.405 220.726 155.405C219.767 155.405 218.99 156.305 218.99 157.141C218.99 157.976 219.767 158.877 220.726 158.877C221.685 158.877 222.462 157.976 222.462 157.141Z" fill="#FCD400"/>
<path d="M36.7107 77.2863C36.7107 76.4222 35.9339 75.5503 34.9748 75.5503C34.0165 75.5503 33.2388 76.4222 33.2388 77.2863C33.2388 78.1504 34.0165 79.0223 34.9748 79.0223C35.9339 79.0223 36.7107 78.1504 36.7107 77.2863Z" fill="#90BFFF"/>
<path d="M210.309 134.574C210.309 133.709 209.532 132.838 208.573 132.838C207.614 132.838 206.837 133.709 206.837 134.574C206.837 135.437 207.614 136.309 208.573 136.309C209.532 136.309 210.309 135.437 210.309 134.574Z" fill="#4E6AF0"/>
<path d="M50.5994 124.157C50.5994 123.291 49.8217 122.421 48.8634 122.421C47.9043 122.421 47.1274 123.291 47.1274 124.157C47.1274 125.023 47.9043 125.893 48.8634 125.893C49.8217 125.893 50.5994 125.023 50.5994 124.157Z" fill="#4E6AF0"/>
<path d="M192.95 96.382C192.95 95.5179 192.173 94.646 191.214 94.646C190.255 94.646 189.478 95.5179 189.478 96.382C189.478 97.2453 190.255 98.118 191.214 98.118C192.173 98.118 192.95 97.2453 192.95 96.382Z" fill="#EAF3F3"/>
<path d="M19.3514 157.141C19.3514 156.277 18.5736 155.405 17.6154 155.405C16.6562 155.405 15.8794 156.277 15.8794 157.141C15.8794 158.005 16.6562 158.877 17.6154 158.877C18.5736 158.877 19.3514 158.005 19.3514 157.141Z" fill="#EAF3F3"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Просмотреть файл

@ -0,0 +1,19 @@
<svg width="165" height="137" viewBox="0 0 165 137" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_327_1711)">
<path d="M71.9785 64.9895H8.83821C4.96316 64.9895 1.82568 61.8451 1.82568 57.9738V8.84188C1.82568 4.97057 4.96866 1.82617 8.83821 1.82617H156.156C160.031 1.82617 163.169 4.97057 163.169 8.84188V44.0801" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.8867 19.3706V47.4444" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M41.622 25.709L18.157 41.1061" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M41.5616 41.2001L18.2122 25.6157" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M71.9785 19.3706V47.4444" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M83.7138 25.709L60.2488 41.1061" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M83.6476 41.2001L60.3037 25.6157" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M149.144 79.0317H100.034C96.1611 79.0317 93.0215 82.1728 93.0215 86.0474V128.158C93.0215 132.033 96.1611 135.174 100.034 135.174H149.144C153.017 135.174 156.156 132.033 156.156 128.158V86.0474C156.156 82.1728 153.017 79.0317 149.144 79.0317Z" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M124.589 116.313C129.432 116.313 133.357 112.385 133.357 107.54C133.357 102.696 129.432 98.7681 124.589 98.7681C119.746 98.7681 115.821 102.696 115.821 107.54C115.821 112.385 119.746 116.313 124.589 116.313Z" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M142.126 68.4974C142.126 58.8054 134.271 50.9526 124.589 50.9526C114.907 50.9526 107.052 58.8109 107.052 68.4974V79.0265" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_327_1711">
<rect width="165" height="137" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.1 KiB

Просмотреть файл

@ -0,0 +1,13 @@
<svg width="184" height="176" viewBox="0 0 184 176" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_327_1722)">
<path d="M161.92 103.54C174.83 97.5702 182.93 84.4602 182.48 70.2402C182.16 48.8402 164.56 31.7502 143.16 32.0702C142.68 32.0702 142.21 32.0902 141.73 32.1202C132.37 13.2602 113.07 1.38022 92.0102 1.50022C63.3102 1.11022 39.2602 23.1202 37.1102 51.7402C21.3102 48.2702 5.69017 58.2602 2.22017 74.0602C1.81017 75.9402 1.58017 77.8602 1.54017 79.7802C0.86017 92.6002 9.56017 104.03 22.1002 106.77" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M92.0102 174.23C119.265 174.23 141.36 152.135 141.36 124.88C141.36 97.6245 119.265 75.5298 92.0102 75.5298C64.7549 75.5298 42.6602 97.6245 42.6602 124.88C42.6602 152.135 64.7549 174.23 92.0102 174.23Z" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M76.5898 112.88C76.5898 104.36 83.4898 97.46 92.0098 97.46C100.53 97.46 107.43 104.36 107.43 112.88C107.43 121.4 100.53 128.3 92.0098 128.3V136.52" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M92.0102 152.98C90.8702 152.98 89.9502 153.9 89.9502 155.04C89.9502 156.18 90.8702 157.1 92.0102 157.1C93.1502 157.1 94.0702 156.18 94.0702 155.04C94.0702 153.9 93.1502 152.98 92.0102 152.98Z" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_327_1722">
<rect width="184" height="175.72" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Просмотреть файл

@ -0,0 +1,11 @@
<svg width="159" height="120" viewBox="0 0 159 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1403_86994)">
<path d="M157.719 20.8536V105.67C157.719 112.878 151.884 118.718 144.683 118.718H14.317C7.11572 118.718 1.28125 112.878 1.28125 105.67V20.8536" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M157.719 20.8535V14.3299C157.719 7.12216 151.884 1.28247 144.683 1.28247H14.317C7.11572 1.28247 1.28125 7.12216 1.28125 14.3299V20.8535L69.0739 63.296C75.4123 67.2632 83.4593 67.2632 89.7978 63.296L157.719 20.8535Z" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1403_86994">
<rect width="159" height="120" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 792 B

Просмотреть файл

@ -0,0 +1,14 @@
<svg width="140" height="153" viewBox="0 0 140 153" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1403_86997)">
<path d="M112.446 69.8338C112.458 93.2796 93.4608 112.294 70.0142 112.31H70.0017C46.4471 111.92 27.6076 92.6192 27.7904 69.0654C27.7987 46.5624 45.7907 28.1921 68.2943 27.7103C68.905 27.6854 69.5198 27.6729 70.1305 27.6729C93.3694 27.5441 112.317 46.2759 112.446 69.5098C112.446 69.6178 112.446 69.7258 112.446 69.8379V69.8338Z" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M68.2935 27.7103C46.2095 51.6296 46.2095 82.8422 68.2935 112.285" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M71.9531 27.7103C94.0413 51.6296 94.0413 82.8297 71.9531 112.269" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27.6719 69.9917H112.327" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.625 22.9506V72.6706C2.0155 106.325 23.3974 136.151 55.1483 147.336L62.4474 150.031C67.3244 151.821 72.6793 151.821 77.5564 150.031L84.8554 147.336C116.606 136.151 137.984 106.325 138.379 72.6706V22.9506C138.366 18.9592 135.973 15.3582 132.297 13.7965C112.568 5.5687 91.3775 1.42778 69.9998 1.6313C48.6262 1.42778 27.4353 5.5687 7.70682 13.7965C4.03031 15.3582 1.63746 18.9592 1.625 22.9506Z" stroke="#010101" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1403_86997">
<rect width="140" height="153" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Просмотреть файл

@ -0,0 +1,13 @@
<svg width="136" height="135" viewBox="0 0 136 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1393_62146)">
<path d="M48.902 41.9401C54.2613 36.6001 54.2613 27.9481 48.902 22.6141L34.3315 8.11805C28.9662 2.78405 20.2733 2.78405 14.9141 8.11805L6.92047 16.0681C0.0119623 22.9561 -1.07314 33.7321 4.32225 41.8501C27.8389 77.0821 58.1917 107.292 93.5903 130.698C101.753 136.068 112.58 134.988 119.506 128.106L127.494 120.156C132.853 114.816 132.853 106.164 127.494 100.83L112.935 86.3281C107.576 80.9881 98.8772 80.9881 93.512 86.3281L88.6591 91.1641C72.3705 77.8261 57.4261 62.9521 44.0251 46.7401L48.9081 41.9341L48.902 41.9401Z" stroke="#010101" stroke-width="3" stroke-miterlimit="10"/>
<path d="M74.873 38.364C87.3457 38.364 97.4553 48.426 97.4553 60.84" stroke="#010101" stroke-width="3" stroke-miterlimit="10"/>
<path d="M74.873 19.6321C97.7386 19.6321 116.276 38.0821 116.276 60.8401" stroke="#010101" stroke-width="3" stroke-miterlimit="10"/>
<path d="M74.873 0.899902C108.132 0.899902 135.096 27.7379 135.096 60.8399" stroke="#010101" stroke-width="3" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_1393_62146">
<rect width="136" height="135" fill="white"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Просмотреть файл

@ -2,113 +2,49 @@
* 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 Link from "next/link";
import Image from "next/image";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../../../api/utils/auth";
import { getSubscriberBreaches } from "../../../../../../functions/server/getUserBreaches";
import { getSubscriberEmails } from "../../../../../../functions/server/getSubscriberEmails";
import { getGuidedExperienceBreaches } from "../../../../../../functions/universal/guidedExperienceBreaches";
import { redirect } from "next/navigation";
import { ReactNode } from "react";
import { FixNavigation } from "../../../../../../components/client/FixNavigation";
import styles from "./fix.module.scss";
import ImageArrowLeft from "./images/icon-arrow-left.svg";
import ImageArrowRight from "./images/icon-arrow-right.svg";
import { FixView } from "./FixView";
import { getLatestOnerepScan } from "../../../../../../../db/tables/onerep_scans";
import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers";
import { canSubscribeToPremium } from "../../../../../../functions/universal/user";
import { getCountryCode } from "../../../../../../functions/server/getCountryCode";
import { headers } from "next/headers";
import imageClose from "./images/icon-close.svg";
import stepDataBrokerProfilesIcon from "./images/step-counter-data-broker-profiles.svg";
import stepHighRiskDataBreachesIcon from "./images/step-counter-high-risk.svg";
import stepLeakedPasswordsIcon from "./images/step-counter-leaked-passwords.svg";
import stepSecurityRecommendationsIcon from "./images/step-counter-security-recommendations.svg";
import { usePathname } from "next/navigation";
export default async function Layout({ children }: { children: ReactNode }) {
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const breaches = await getSubscriberBreaches(session.user);
const subscriberEmails = await getSubscriberEmails(session.user);
const guidedExperience = getGuidedExperienceBreaches(
breaches,
subscriberEmails
);
// TODO:
// Add logic to protect routes for specific users (premium/not, scan started/not)
// Question: Can FXA redirect user back to specific URL (for returning upgrade users during fix data broker)
const headersList = headers();
const countryCode = getCountryCode(headersList);
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] as number;
if (
!profileId &&
canSubscribeToPremium({ user: session?.user, countryCode: countryCode })
) {
return redirect("/redesign/user/welcome/");
}
const scanResult = await getLatestOnerepScan(profileId);
const scanResultItems = scanResult?.onerep_scan_results?.data ?? [];
function NavigationClose() {
return (
<Link href="redesign/user/dashboard" className={styles.navClose}>
<Image alt="" src={imageClose} />
</Link>
<FixView breaches={guidedExperience} userScannedResults={scanResultItems}>
{children}
</FixView>
);
}
function NavigationArrowBack() {
return (
// FIXME: This navigation arrow should point to the previous step in whichever context it is loaded
<Link className={styles.navArrowBack} href="/redesign/user/dashboard">
<Image alt="" src={ImageArrowLeft} />
</Link>
);
}
function NavigationArrowNext() {
return (
// FIXME: This navigation arrow should point to the next step in whichever context it is loaded
<Link className={styles.navArrowNext} href="/redesign/user/dashboard">
<Image alt="" src={ImageArrowRight} />
</Link>
);
}
export type FixLayoutProps = {
children: ReactNode;
};
const FixLayout = (props: FixLayoutProps) => {
const pathname = usePathname();
const isHighRiskDataBreach = pathname.includes("high-risk-data-breaches");
const navigationItemsContent = [
{
key: "data-broker-profiles",
labelStringId: "fix-flow-nav-data-broker-profiles",
href: "/redesign/user/dashboard/fix/data-broker-profiles",
status: "#",
currentStepId: "dataBrokerProfiles",
imageId: stepDataBrokerProfilesIcon,
},
{
key: "high-risk-data-breaches",
labelStringId: "fix-flow-nav-high-risk-data-breaches",
href: "/redesign/user/dashboard/fix/high-risk-data-breaches",
status: "#",
currentStepId: "highRiskDataBreaches",
imageId: stepHighRiskDataBreachesIcon,
},
{
key: "leaked-passwords",
labelStringId: "fix-flow-nav-leaked-passwords",
href: "/redesign/user/dashboard/fix/leaked-passwords",
status: "#",
currentStepId: "leakedPasswords",
imageId: stepLeakedPasswordsIcon,
},
{
key: "security-recommendations",
labelStringId: "fix-flow-nav-security-recommendations",
href: "/redesign/user/dashboard/fix/security-recommendations",
status: "#",
currentStepId: "securityRecommendations",
imageId: stepSecurityRecommendationsIcon,
},
];
return (
<div className={styles.fixContainer}>
<div
className={`${styles.fixWrapper} ${
isHighRiskDataBreach ? styles.highRiskDataBreachContentBg : ""
}`}
>
<FixNavigation navigationItems={navigationItemsContent} />
<NavigationClose />
<section className={styles.fixSection}>
<NavigationArrowBack />
<NavigationArrowNext />
{props.children}
</section>
</div>
</div>
);
};
export default FixLayout;

Просмотреть файл

@ -0,0 +1,60 @@
/* 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 passwordIllustration from "../images/leaked-passwords.svg";
import securityQuestionsIllustration from "../images/security-questions.svg";
import { LeakedPasswordsLayout } from "./LeakedPasswordsLayout";
const meta: Meta<typeof LeakedPasswordsLayout> = {
title: "ResolutionLayout",
component: LeakedPasswordsLayout,
};
export default meta;
type Story = StoryObj<typeof LeakedPasswordsLayout>;
const summaryString = "It appeared in 2 data breaches:";
const recommendations = {
title: "Heres what to do",
steps: (
<ol>
<li>Recommendation one</li>
<li>Recommendation two</li>
<li>Recommendation three</li>
</ol>
),
};
const content = {
summary: summaryString,
description: (
<p>
Leaked passwords / Security questions leaked recommendation description
text.
</p>
),
recommendations,
};
export const Passwords: Story = {
args: {
pageData: {
type: "password",
title: "Passwords",
illustration: passwordIllustration,
content,
},
},
};
export const SecurityQuestions: Story = {
args: {
pageData: {
type: "security-question",
title: "Security questions",
illustration: securityQuestionsIllustration,
content,
},
},
};

Просмотреть файл

@ -0,0 +1,27 @@
/* 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 { render } from "@testing-library/react";
import { it, expect } from "@jest/globals";
import { composeStory } from "@storybook/react";
import { axe } from "jest-axe";
import Meta, {
Passwords,
SecurityQuestions,
} from "./LeakedPasswordsLayout.stories";
it("leaked passwords component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(Passwords, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("security questions component passes the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(
SecurityQuestions,
Meta
);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});

Просмотреть файл

@ -0,0 +1,60 @@
/* 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 { ResolutionContainer } from "../ResolutionContainer";
import { ResolutionContent } from "../ResolutionContent";
import { Button } from "../../../../../../../components/server/Button";
import { useL10n } from "../../../../../../../hooks/l10n";
import { LeakedPassword } from "./leakedPasswordsData";
import Link from "next/link";
export interface LeakedPasswordsLayoutProps {
label: string;
pageData: LeakedPassword;
locale: string;
}
export function LeakedPasswordsLayout({
pageData,
locale,
}: LeakedPasswordsLayoutProps) {
const l10n = useL10n();
const { title, illustration, content } = pageData;
return (
<ResolutionContainer
type="leakedPasswords"
title={title}
illustration={illustration}
cta={
<>
<Button
variant="primary"
small
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore next 3 */
onClick={() => {
// TODO: MNTOR-1700 Add routing logic
}}
autoFocus={true}
>
{l10n.getString("leaked-passwords-mark-as-fixed")}
</Button>
<Link
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore next */
href="/"
>
{l10n.getString("leaked-passwords-skip")}
</Link>
</>
}
estimatedTime={4}
>
<ResolutionContent content={content} locale={locale} />
</ResolutionContainer>
);
}

Просмотреть файл

@ -0,0 +1,57 @@
/* 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 { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import {
getL10n,
getLocale,
} from "../../../../../../../../functions/server/l10n";
import { LeakedPasswordsLayout } from "../LeakedPasswordsLayout";
import { getLeakedPasswords } from "../leakedPasswordsData";
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
interface LeakedPasswordsProps {
params: {
type: string;
};
}
export default async function LeakedPasswords({
params,
}: LeakedPasswordsProps) {
const session = await getServerSession(authOptions);
const locale = getLocale();
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const l10n = getL10n();
const breaches = await getSubscriberBreaches(session.user);
const subscriberEmails = await getSubscriberEmails(session.user);
const guidedExperienceBreaches = getGuidedExperienceBreaches(
breaches,
subscriberEmails
);
const { type } = params;
const pageData = getLeakedPasswords({
dataType: type,
breaches: guidedExperienceBreaches,
});
if (!pageData) {
redirect("/redesign/user/dashboard");
}
return (
<LeakedPasswordsLayout
label={l10n.getString("security-recommendation-steps-label")}
pageData={pageData}
locale={locale}
/>
);
}

Просмотреть файл

@ -0,0 +1,157 @@
/* 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 { ReactNode } from "react";
import passwordIllustration from "../images/leaked-passwords.svg";
import securityQuestionsIllustration from "../images/security-questions.svg";
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { getL10n } from "../../../../../../../functions/server/l10n";
export type LeakedPasswordsContent = {
summary: string;
description: ReactNode;
recommendations?: {
title: string;
steps: ReactNode;
subtitle?: string;
};
};
export type LeakedPasswordsTypes = "password" | "security-question";
export type LeakedPassword = {
type: LeakedPasswordsTypes;
title: string;
illustration: string;
content: LeakedPasswordsContent;
};
function getLeakedPasswords({
dataType,
breaches,
}: {
dataType: string;
breaches: GuidedExperienceBreaches;
}) {
const l10n = getL10n();
const findFirstUnresolvedBreach = (breachClassType: LeakedPasswordsTypes) => {
const leakedPasswordType =
breachClassType === "password" ? "passwords" : "securityQuestions";
return Object.values(breaches.passwordBreaches[leakedPasswordType]).find(
(breach) => !breach.isResolved
);
};
const unresolvedPasswordBreach = findFirstUnresolvedBreach("password");
const unresolvedSecurityQuestionsBreach =
findFirstUnresolvedBreach("security-question");
const blockList = (process.env.HIBP_BREACH_DOMAIN_BLOCKLIST ?? "").split(",");
const getBreachInfo = (breach?: SubscriberBreach) => ({
name: breach ? breach.name : "",
breachDate: breach ? breach.breachDate : "",
breachSite:
breach && !blockList.includes(breach.domain)
? `https://${breach.domain}`
: "",
});
const {
name: passwordBreachName,
breachDate: passwordBreachDate,
breachSite: passwordBreachSite,
} = getBreachInfo(unresolvedPasswordBreach);
const {
name: securityQuestionBreachName,
breachDate: securityQuestionBreachDate,
breachSite: securityQuestionBreachSite,
} = getBreachInfo(unresolvedSecurityQuestionsBreach);
const leakedPasswordsData: LeakedPassword[] = [
{
type: "password",
title: l10n.getString("leaked-passwords-title", {
breach_name: passwordBreachName,
}),
illustration: passwordIllustration,
content: {
summary: l10n.getString("leaked-passwords-summary", {
breach_date: passwordBreachDate,
}),
description: <p>{l10n.getString("leaked-passwords-description")}</p>,
recommendations: {
title: l10n.getString("leaked-passwords-steps-title"),
steps: (
<ol>
<li>
{l10n.getFragment("leaked-passwords-step-one", {
elems: {
// TODO: Find a way to go to the actual breach site
link_to_breach_site: (
<a
href={passwordBreachSite}
target="_blank"
rel="noopener noreferrer"
/>
),
},
vars: {
breach_name: passwordBreachName,
},
})}
</li>
<li>{l10n.getString("leaked-passwords-step-two")}</li>
</ol>
),
},
},
},
{
type: "security-question",
title: l10n.getString("leaked-security-questions-title"),
illustration: securityQuestionsIllustration,
content: {
summary: l10n.getString("leaked-security-questions-summary", {
breach_name: securityQuestionBreachName,
breach_date: securityQuestionBreachDate,
}),
description: (
<p>{l10n.getString("leaked-security-questions-description")}</p>
),
recommendations: {
title: l10n.getString("leaked-security-questions-steps-title"),
steps: (
<ol>
<li>
{l10n.getFragment("leaked-security-questions-step-one", {
elems: {
// TODO: Find a way to go to the actual breach site
link_to_breach_site: (
<a
href={securityQuestionBreachSite}
target="_blank"
rel="noopener noreferrer"
/>
),
},
vars: {
breach_name: securityQuestionBreachName,
},
})}
</li>
<li>{l10n.getFragment("leaked-security-questions-step-two")}</li>
</ol>
),
},
},
},
];
return leakedPasswordsData.find((content) => content.type === dataType);
}
export { getLeakedPasswords };

Просмотреть файл

@ -1,7 +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/. */
export default function LeakedPasswords() {
return <div>LeakedPasswords</div>;
}

Просмотреть файл

@ -0,0 +1,72 @@
/* 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 type { SecurityRecommendation } from "./securityRecommendationsData";
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
import { createRandomBreach } from "../../../../../../../../apiMocks/mockData";
import { SecurityRecommendationsLayout } from "./SecurityRecommendationsLayout";
import phoneIllustration from "../images/security-recommendations-phone.svg";
const meta: Meta<typeof SecurityRecommendationsLayout> = {
title: "SecurityRecommendationsLayout",
component: SecurityRecommendationsLayout,
};
export default meta;
type Story = StoryObj<typeof SecurityRecommendationsLayout>;
const scannedResultsArraySample: SubscriberBreach[] = Array.from(
{ length: 5 },
() => createRandomBreach({ isHighRiskOnly: true })
);
const pageDummyData: SecurityRecommendation = {
type: "phone",
title: "Dummy title",
illustration: phoneIllustration,
exposedData: scannedResultsArraySample,
content: {
summary: "It appeared in 2 data breaches:",
description: <p>Security recommendatino description text.</p>,
recommendations: {
title: "Here",
steps: (
<ol>
<li>Recommendation one</li>
<li>Recommendation two</li>
<li>Recommendation three</li>
</ol>
),
},
},
};
export const Layout: Story = {
args: {
label: "Security recommendations",
pageData: pageDummyData,
},
};
export const Phone: Story = {
args: {
label: "Security recommendations",
pageData: pageDummyData,
},
};
export const Email: Story = {
args: {
label: "Security recommendations",
pageData: { ...pageDummyData, type: "email" },
},
};
export const Ip: Story = {
args: {
label: "Security recommendations",
pageData: { ...pageDummyData, type: "ip" },
},
};

Просмотреть файл

@ -0,0 +1,27 @@
/* 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 { render } from "@testing-library/react";
import { it, expect } from "@jest/globals";
import { composeStory } from "@storybook/react";
import { axe } from "jest-axe";
import Meta, { Phone, Email, Ip } from "./SecurityRecommendations.stories";
it("Phone security recommendations the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(Phone, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("Email security recommendations the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(Email, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});
it("IP security recommendations the axe accessibility test suite", async () => {
const ComposedHighRiskDataBreachComponent = composeStory(Ip, Meta);
const { container } = render(<ComposedHighRiskDataBreachComponent />);
expect(await axe(container)).toHaveNoViolations();
});

Просмотреть файл

@ -0,0 +1,55 @@
/* 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 type { SecurityRecommendation } from "./securityRecommendationsData";
import { ResolutionContainer } from "../ResolutionContainer";
import { ResolutionContent } from "../ResolutionContent";
import { Button } from "../../../../../../../components/server/Button";
import { useL10n } from "../../../../../../../hooks/l10n";
export interface SecurityRecommendationsLayoutProps {
label: string;
pageData: SecurityRecommendation;
locale: string;
}
export function SecurityRecommendationsLayout({
label,
pageData,
locale,
}: SecurityRecommendationsLayoutProps) {
const l10n = useL10n();
const { title, illustration, content, exposedData } = pageData;
return (
<ResolutionContainer
label={label}
type="securityRecommendations"
title={title}
illustration={illustration}
cta={
<Button
variant="primary"
small
// TODO: Add test once MNTOR-1700 logic is added
/* c8 ignore next 3 */
onClick={() => {
// TODO: MNTOR-1700 Add routing logic
}}
autoFocus={true}
>
{l10n.getString("security-recommendation-steps-cta-label")}
</Button>
}
>
<ResolutionContent
content={content}
exposedData={exposedData}
locale={locale}
/>
</ResolutionContainer>
);
}

Просмотреть файл

@ -0,0 +1,57 @@
/* 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 { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { SecurityRecommendationsLayout } from "../SecurityRecommendationsLayout";
import { getSecurityRecommendationsByType } from "../securityRecommendationsData";
import { authOptions } from "../../../../../../../../api/utils/auth";
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import {
getL10n,
getLocale,
} from "../../../../../../../../functions/server/l10n";
interface SecurityRecommendationsProps {
params: {
type: string;
};
}
export default async function SecurityRecommendations({
params,
}: SecurityRecommendationsProps) {
const session = await getServerSession(authOptions);
const locale = getLocale();
if (!session?.user?.subscriber?.id) {
return redirect("/");
}
const l10n = getL10n();
const breaches = await getSubscriberBreaches(session.user);
const subscriberEmails = await getSubscriberEmails(session.user);
const guidedExperienceBreaches = getGuidedExperienceBreaches(
breaches,
subscriberEmails
);
const { type } = params;
const pageData = getSecurityRecommendationsByType({
dataType: type,
breaches: guidedExperienceBreaches,
});
if (!pageData) {
redirect("/redesign/user/dashboard");
}
return (
<SecurityRecommendationsLayout
label={l10n.getString("security-recommendation-steps-label")}
pageData={pageData}
locale={locale}
/>
);
}

Просмотреть файл

@ -1,7 +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/. */
export default function SecurityRecommendations() {
return <div>SecurityRecommendations</div>;
}

Просмотреть файл

@ -0,0 +1,177 @@
/* 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 { ReactNode } from "react";
import emailIllustration from "../images/security-recommendations-email.svg";
import phoneIllustration from "../images/security-recommendations-phone.svg";
import ipIllustration from "../images/security-recommendations-ip.svg";
import { getL10n } from "../../../../../../../functions/server/l10n";
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
export type SecurityRecommendationContent = {
summary: string;
description: ReactNode;
recommendations?: {
title: string;
steps: ReactNode;
subtitle?: string;
};
};
export type SecurityRecommendationTypes = "phone" | "email" | "ip";
export type SecurityRecommendation = {
type: SecurityRecommendationTypes;
title: string;
illustration: string;
content: SecurityRecommendationContent;
exposedData: SubscriberBreach[];
};
function getSecurityRecommendationsByType({
dataType,
breaches,
}: {
dataType: string;
breaches: GuidedExperienceBreaches;
}) {
const l10n = getL10n();
const securityRecommendationsData: SecurityRecommendation[] = [
{
type: "phone",
title: l10n.getString("security-recommendation-phone-title"),
illustration: phoneIllustration,
exposedData: breaches.securityRecommendations.phoneNumber,
content: {
summary: l10n.getString("security-recommendation-phone-summary", {
num_breaches: breaches.securityRecommendations.phoneNumber.length,
}),
description: (
<p>{l10n.getString("security-recommendation-phone-description")}</p>
),
recommendations: {
title: l10n.getString("security-recommendation-steps-title"),
steps: (
<ol>
<li>
{l10n.getString("security-recommendation-phone-step-one")}
</li>
<li>
{l10n.getString("security-recommendation-phone-step-two")}
</li>
<li>
{l10n.getFragment("security-recommendation-phone-step-three", {
elems: {
link_to_info: (
<a
href="https://relay.firefox.com/?utm_medium=monitor&utm_source=security-reco&utm_campaign=fxmonitor-xsell&utm_content=phone-recs-global"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
</ol>
),
},
},
},
{
type: "email",
title: l10n.getString("security-recommendation-email-title"),
illustration: emailIllustration,
exposedData: breaches.securityRecommendations.emailAddress,
content: {
summary: l10n.getString("security-recommendation-email-summary", {
num_breaches: breaches.securityRecommendations.emailAddress.length,
}),
description: (
<p>{l10n.getString("security-recommendation-email-description")}</p>
),
recommendations: {
title: l10n.getString("security-recommendation-steps-title"),
steps: (
<ol>
<li>
{l10n.getString("security-recommendation-email-step-one")}
</li>
<li>
{l10n.getFragment("security-recommendation-email-step-two", {
elems: {
link_to_info: (
<a
href="https://consumer.ftc.gov/articles/how-recognize-and-avoid-phishing-scams"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
<li>
{l10n.getString("security-recommendation-email-step-three")}
</li>
<li>
{l10n.getFragment("security-recommendation-email-step-four", {
elems: {
link_to_info: (
<a
href="https://relay.firefox.com/?utm_medium=monitor&utm_source=security-reco&utm_campaign=fxmonitor-xsell&utm_content=email-recs-global"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
</ol>
),
},
},
},
{
type: "ip",
title: l10n.getString("security-recommendation-ip-title"),
illustration: ipIllustration,
exposedData: breaches.securityRecommendations.IPAddress,
content: {
summary: l10n.getString("security-recommendation-ip-summary", {
num_breaches: breaches.securityRecommendations.IPAddress.length,
}),
description: (
<p>{l10n.getString("security-recommendation-ip-description")}</p>
),
recommendations: {
title: l10n.getString("security-recommendation-steps-title"),
steps: (
<ul className="noList">
<li>
{l10n.getFragment("security-recommendation-ip-step-one", {
elems: {
link_to_info: (
<a
href="https://www.mozilla.org/products/vpn/?entrypoint_experiment=vpn-refresh-pricing&entrypoint_variation=1"
target="_blank"
rel="noopener noreferrer"
/>
),
},
})}
</li>
</ul>
),
},
},
},
];
return securityRecommendationsData.find(
(content) => content.type === dataType
);
}
export { getSecurityRecommendationsByType };

Просмотреть файл

@ -7,12 +7,17 @@ import { getServerSession } from "next-auth";
import { getL10n, getL10nBundles } from "../../../../../functions/server/l10n";
import { authOptions } from "../../../../../api/utils/auth";
import { Shell } from "../../../Shell";
import { SignInButton } from "../../../../../(nextjs_migration)/components/client/SignInButton";
export default async function Layout({ children }: { children: ReactNode }) {
const l10nBundles = getL10nBundles();
const l10n = getL10n(l10nBundles);
const session = await getServerSession(authOptions);
if (!session) {
return <SignInButton autoSignIn={true} />;
}
return (
<Shell l10n={l10n} session={session}>
{children}

Просмотреть файл

@ -52,6 +52,9 @@ export default async function DashboardPage() {
session.user
);
const featureFlagsEnabled = { FreeBrokerScan, PremiumBrokerRemoval };
const isAllFixed =
summary.dataBreachFixedNum === summary.dataBreachTotalNum &&
summary.dataBrokerFixedNum === summary.dataBrokerTotalNum;
return (
<View
@ -63,6 +66,7 @@ export default async function DashboardPage() {
locale={locale}
bannerData={summary}
featureFlagsEnabled={featureFlagsEnabled}
isAllFixed={isAllFixed}
/>
);
}

Просмотреть файл

@ -52,7 +52,8 @@ export const FindExposures = ({
const maxProgress = 100;
const labelSwitchThreshold = 50;
const percentageSteps = 6;
const scanDurationInSeconds = 60;
const percentageSteps = maxProgress / scanDurationInSeconds;
const breachesScannedCount = getCurrentScanCountForRange({
totalCount: breachesTotalCount,
currentProgress: scanProgress,
@ -101,7 +102,13 @@ export const FindExposures = ({
}
return () => clearTimeout(timeoutId);
}, [scanProgress, router, checkingScanProgress, scanFinished]);
}, [
scanProgress,
router,
checkingScanProgress,
scanFinished,
percentageSteps,
]);
function ProgressLabel() {
return (

Просмотреть файл

@ -18,7 +18,7 @@ import { PageLink } from "./PageLink";
export type Props = {
children: ReactNode;
session: Session | null;
session: Session;
};
export const MobileShell = (props: Props) => {
@ -102,7 +102,7 @@ export const MobileShell = (props: Props) => {
</ul>
{process.env.NEXT_PUBLIC_PREMIUM_ENABLED === "true" && (
<div className={styles.premiumCta}>
<PremiumBadge user={props.session?.user ?? null} />
<PremiumBadge user={props.session.user} />
</div>
)}
</div>

Просмотреть файл

@ -15,7 +15,7 @@ import { ExtendedReactLocalization } from "../../hooks/l10n";
export type Props = {
l10n: ExtendedReactLocalization;
session: Session | null;
session: Session;
children: ReactNode;
};

Просмотреть файл

@ -240,7 +240,6 @@ export async function POST(request: NextRequest) {
const oneRepProfileId = result?.[0]?.["onerep_profile_id"] as number;
console.debug("fxa_subscription_change", JSON.stringify(result));
console.debug("fxa_subscription_change", { oneRepProfileId });
// MNTOR-2103: if one rep profile id doesn't exist in the db, fail silently
if (!oneRepProfileId) {
@ -259,23 +258,31 @@ export async function POST(request: NextRequest) {
break;
}
if (
updatedSubscriptionFromEvent.isActive &&
updatedSubscriptionFromEvent.capabilities.includes(
MONITOR_PREMIUM_CAPABILITY
)
) {
// activate and opt out profiles
await activateProfile(oneRepProfileId);
await optoutProfile(oneRepProfileId);
} else if (
!updatedSubscriptionFromEvent.isActive &&
updatedSubscriptionFromEvent.capabilities.includes(
MONITOR_PREMIUM_CAPABILITY
)
) {
// deactivation stops opt out process
await deactivateProfile(oneRepProfileId);
try {
if (
updatedSubscriptionFromEvent.isActive &&
updatedSubscriptionFromEvent.capabilities.includes(
MONITOR_PREMIUM_CAPABILITY
)
) {
// activate and opt out profiles
await activateProfile(oneRepProfileId);
await optoutProfile(oneRepProfileId);
} else if (
!updatedSubscriptionFromEvent.isActive &&
updatedSubscriptionFromEvent.capabilities.includes(
MONITOR_PREMIUM_CAPABILITY
)
) {
// deactivation stops opt out process
await deactivateProfile(oneRepProfileId);
}
} catch (e) {
captureException(
new Error(`${(e as Error).message}\n
Event: ${event}\n
updateFromEvent: ${JSON.stringify(updatedSubscriptionFromEvent)}`)
);
}
break;
}

Просмотреть файл

@ -0,0 +1,73 @@
/* 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 { NextRequest, NextResponse } from "next/server";
import { bearerToken } from "../../../utils/auth";
import { PubSub } from "@google-cloud/pubsub";
const projectId = process.env.GCP_PUBSUB_PROJECT_ID;
const topicName = process.env.GCP_PUBSUB_TOPIC_NAME;
const subscriptionName = process.env.GCP_PUBSUB_SUBSCRIPTION_NAME;
/**
* Whenever a breach is detected on the HIBP side, HIBP sends a request to this endpoint.
* The payload is checked for validity, and immediately queued if it is valid.
*
* @param req
*/
export async function POST(req: NextRequest) {
if (!projectId) {
throw new Error("GCP_PUBSUB_PROJECT_ID env var not set");
}
if (!topicName) {
throw new Error("GCP_PUBSUB_TOPIC_NAME env var not set");
}
const headerToken = bearerToken(req);
if (headerToken !== process.env.HIBP_NOTIFY_TOKEN) {
return NextResponse.json({ success: false }, { status: 401 });
}
const json = await req.json();
if (!(json.breachName && json.hashPrefix && json.hashSuffixes)) {
console.error(
"HIBP breach notification: requires breachName, hashPrefix, and hashSuffixes."
);
return NextResponse.json({ success: false }, { status: 400 });
}
const pubsub = new PubSub({ projectId });
const [topics] = await pubsub.getTopics();
const [topic] = topics.filter(
(a) => a.name === `projects/${projectId}/topics/${topicName}`
);
if (!topic || topic.name !== `projects/${projectId}/topics/${topicName}`) {
if (process.env.NODE_ENV === "development") {
try {
if (!subscriptionName) {
throw new Error("GCP_PUBSUB_SUBSCRIPTION_NAME env var not set");
}
await pubsub.createTopic(topicName);
await pubsub.topic(topicName).createSubscription(subscriptionName);
} catch (ex) {
console.debug(ex);
}
} else {
console.error("Topic not found:", topicName);
return NextResponse.json({ success: false }, { status: 500 });
}
}
try {
await topic.publishMessage({ json });
return NextResponse.json({ success: true }, { status: 200 });
} catch (ex) {
console.error("Error queueing HIBP breach:", ex);
return NextResponse.json({ success: false }, { status: 500 });
}
}

Просмотреть файл

@ -8,7 +8,7 @@ import { captureException } from "@sentry/node";
import crypto from "crypto";
import { setOnerepScanResults } from "../../../../db/tables/onerep_scans";
import { getAllScanResults } from "../../../functions/server/onerep";
import { getAllScanResults, Scan } from "../../../functions/server/onerep";
interface OnerepWebhookRequest {
id: number;
@ -19,7 +19,7 @@ interface OnerepWebhookRequest {
id: number;
profile_id: number;
status: "finished";
reason: "manual" | "initial" | "monitoring";
reason: Scan["reason"];
created_at: string;
updated_at: string;
url: string;

Просмотреть файл

@ -12,20 +12,43 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ success: "false" }, { status: 401 });
}
const monthlyQuota = process.env.MONTHLY_SCANS_QUOTA;
const monthlyScanQuota = parseInt(
(process.env.MONTHLY_SCANS_QUOTA as string) || "0"
);
const monthlySubscriberQuota = parseInt(
(process.env.MONTHLY_SUBSCRIBERS_QUOTA as string) || "0"
);
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const result = await getScansCount(
firstDayOfMonth.toDateString(),
now.toDateString()
);
const scansCount = result[0]["count"];
const manualScansCount =
((
await getScansCount(
firstDayOfMonth.toDateString(),
now.toDateString(),
"manual"
)
)?.[0]?.["count"] as string) || "0";
const initialScansCount =
((
await getScansCount(
firstDayOfMonth.toDateString(),
now.toDateString(),
"initial"
)
)?.[0]?.["count"] as string) || "0";
const message = {
monthlyQuota,
scansCount,
scans: {
quota: monthlyScanQuota,
count: parseInt(manualScansCount),
},
subscribers: {
quota: monthlySubscriberQuota,
count: parseInt(initialScansCount),
},
};
return NextResponse.json({ success: true, message }, { status: 200 });

Просмотреть файл

@ -41,12 +41,15 @@
display: flex;
align-items: center;
padding-left: 0;
gap: $layout-md;
margin: 0;
width: 100%;
justify-content: space-between;
height: 30px; // fixed height to standardize image/icon heights
@media screen and (min-width: $screen-lg) {
gap: $layout-md;
}
.fallbackLogo {
display: flex;
align-items: center;
@ -68,10 +71,18 @@
.exposureCompanyTitle {
font: $text-body-sm;
max-width: 200px;
max-width: 150px; // truncate long names for mobile view
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@media screen and (min-width: $screen-sm) {
max-width: 250px; // truncate long names for mobile view
}
@media screen and (min-width: $screen-md) {
max-width: 100%;
}
}
@media screen and (min-width: $screen-lg) {
@ -115,8 +126,12 @@
border-top: 2px solid rgba($color-purple-70, 0.2);
overflow: auto;
height: auto;
padding: $spacing-lg;
display: block;
padding: $spacing-lg;
@media screen and (min-width: $screen-lg) {
padding: $spacing-xl $layout-xl;
}
}
.exposedInfoContainer {

Просмотреть файл

@ -4,9 +4,10 @@
"use client";
import React, { CSSProperties, ReactElement, useState } from "react";
import React, { ReactElement, useState } from "react";
import Link from "next/link";
import styles from "./ExposureCard.module.scss";
import { StatusPill, StatusPillType } from "../server/StatusPill";
import { StatusPill } from "../server/StatusPill";
import Image, { StaticImageData } from "next/image";
import {
ChevronDown,
@ -21,31 +22,15 @@ import {
import { Button } from "../server/Button";
import { useL10n } from "../../hooks/l10n";
import { ScanResult } from "../../functions/server/onerep";
import { FeatureFlagsEnabled } from "../../functions/server/featureFlags";
import {
DataClassEffected,
SubscriberBreach,
} from "../../../utils/subscriberBreaches";
import { parseIso8601Datetime } from "../../../utils/parse";
import { FallbackLogo } from "../server/BreachLogo";
export type Exposure = ScanResult | SubscriberBreach;
export type ExposureTypElProps = {
type: Exposure;
};
export const ExposureTypeEl = (props: ExposureTypElProps) => {
const l10n = useL10n();
let string = "";
if (isScanResult(props.type)) {
string = l10n.getString("exposure-card-exposure-type-data-broker");
} else {
string = l10n.getString("exposure-card-exposure-type-data-breach");
}
return <>{string}</>;
};
// Typeguard function
export function isScanResult(obj: Exposure): obj is ScanResult {
return (obj as ScanResult).data_broker !== undefined; // only ScanResult has an instance of data_broker
@ -53,237 +38,121 @@ export function isScanResult(obj: Exposure): obj is ScanResult {
export type ExposureCardProps = {
exposureImg?: StaticImageData;
exposureName: string;
exposureData: Exposure;
exposureDetailsLink: string;
dateFound: Date;
statusPillType: StatusPillType;
locale: string;
color: string;
featureFlagsEnabled: Pick<FeatureFlagsEnabled, "PremiumBrokerRemoval">;
isPremiumBrokerRemovalEnabled: boolean;
};
type BreachExposureCategoryProps = {
exposureCategoryLabel: string;
icon: ReactElement;
count?: number;
count: number;
emails?: string[];
};
type ScannedExposureCategoryProps = {
exposureCategoryLabel: string;
num: number;
icon: ReactElement;
export const ExposureCard = ({ exposureData, ...props }: ExposureCardProps) => {
return isScanResult(exposureData) ? (
<ScanResultCard {...props} scanResult={exposureData} />
) : (
<SubscriberBreachCard {...props} subscriberBreach={exposureData} />
);
};
export const ExposureCard = (props: ExposureCardProps) => {
const {
exposureImg,
exposureName,
exposureData,
exposureDetailsLink,
statusPillType,
locale,
color,
featureFlagsEnabled,
} = props;
export type ScanResultCardProps = {
exposureImg?: StaticImageData;
scanResult: ScanResult;
locale: string;
isPremiumBrokerRemovalEnabled: boolean;
};
const ScanResultCard = (props: ScanResultCardProps) => {
const { exposureImg, scanResult, locale, isPremiumBrokerRemovalEnabled } =
props;
const l10n = useL10n();
const [exposureCardExpanded, setExposureCardExpanded] = useState(false);
const letsFixItBtn = (
<span className={styles.fixItBtn}>
<Button variant="primary" wide>
{l10n.getString("exposure-card-cta")}
</Button>
</span>
);
const dateFormatter = new Intl.DateTimeFormat(locale, {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle
dateStyle: "medium",
});
const exposureCategoriesArray: React.ReactElement[] = [];
const exposureItem = props.exposureData;
const BreachExposureCategory = (props: BreachExposureCategoryProps) => {
const emailsList = (
<ul className={styles.emailsList}>
{props.emails?.map((email: string, index: number) => (
<li key={index}>{email}</li>
))}
</ul>
);
return (
<div className={styles.detailsFoundItem}>
<dt>
<span className={styles.exposureTypeIcon}>{props.icon}</span>
{props.exposureCategoryLabel}
</dt>
<dd>
{props.emails && emailsList}
{props.count &&
l10n.getString("exposure-card-num-found", {
exposure_num: props.count,
})}
</dd>
</div>
);
type ScannedExposureCategoryProps = {
exposureCategoryLabel: string;
num: number;
icon: ReactElement;
};
const ScannedExposureCategory = (props: ScannedExposureCategoryProps) => {
const description = l10n.getString("exposure-card-num-found", {
exposure_num: props.num,
});
return (
<div className={styles.detailsFoundItem}>
<dt>
<span className={styles.exposureTypeIcon}>{props.icon}</span>
{props.exposureCategoryLabel}
</dt>
<dd>{description}</dd>
</div>
);
};
// Scan Result Categories
if (isScanResult(exposureItem)) {
if (exposureItem.relatives.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="relatives"
icon={<MultipleUsersIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-family-members")}
num={exposureItem.relatives.length}
/>
);
}
if (exposureItem.phones.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="phones"
icon={<PhoneIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-phone-number")}
num={exposureItem.phones.length}
/>
);
}
if (exposureItem.emails.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="emails"
icon={<EmailIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-email")}
num={exposureItem.emails.length}
/>
);
}
if (exposureItem.addresses.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="addresses"
icon={<LocationPinIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-address")}
num={exposureItem.addresses.length}
/>
);
// TODO: Add unit test when changing this code:
/* c8 ignore next 11 */
} else {
// "Other" item when none of the conditions above are met
exposureCategoriesArray.push(
<ScannedExposureCategory
key="other"
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-other")}
num={0}
/>
);
}
}
//Breach Categories
else {
exposureItem.dataClassesEffected.map((item: DataClassEffected) => {
const dataClass = Object.keys(item)[0];
const value = item[dataClass];
const emails = Array.isArray(value) ? value : [];
const count = typeof value === "number" ? value : 0;
if (dataClass === "email-addresses") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<EmailIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-email")}
emails={emails} // Only emails get listed
/>
);
} else if (dataClass === "passwords") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<PasswordIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-password")}
count={count}
/>
);
} else if (dataClass === "phone-numbers") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<PhoneIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-phone-number")}
count={count}
/>
);
} else if (dataClass === "ip-addresses") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-ip-address")}
count={count}
/>
);
// TODO: Add unit test when changing this code:
/* c8 ignore next 12 */
}
// Handle all other breach categories
else {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<QuestionMarkCircle alt="" width="13" height="13" />} // default icon for categories without a unique one
exposureCategoryLabel={l10n.getString(dataClass)} // categories are localized in data-classes.ftl
count={count}
/>
);
}
});
}
const ExposureCategoriesListElem = () => {
const listItems = exposureCategoriesArray.map((item) => (
<React.Fragment key={item.key}>{item}</React.Fragment>
));
return <>{listItems}</>;
};
function fallbackLogo(exposureId: string) {
// TODO: Add unit test when changing this code:
/* c8 ignore next */
const firstLetter = exposureId?.[0]?.toUpperCase() || "";
return (
<span
className={styles.fallbackLogo}
style={{ background: color } as CSSProperties}
>
{firstLetter}
</span>
if (scanResult.relatives.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="relatives"
icon={<MultipleUsersIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-family-members")}
num={scanResult.relatives.length}
/>
);
}
if (scanResult.phones.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="phones"
icon={<PhoneIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-phone-number")}
num={scanResult.phones.length}
/>
);
}
if (scanResult.emails.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="emails"
icon={<EmailIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-email")}
num={scanResult.emails.length}
/>
);
}
if (scanResult.addresses.length > 0) {
exposureCategoriesArray.push(
<ScannedExposureCategory
key="addresses"
icon={<LocationPinIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-address")}
num={scanResult.addresses.length}
/>
);
// TODO: Add unit test when changing this code:
/* c8 ignore next 11 */
} else {
// "Other" item when none of the conditions above are met
exposureCategoriesArray.push(
<ScannedExposureCategory
key="other"
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-other")}
num={0}
/>
);
}
const letsFixItBtn = (
<span className={styles.fixItBtn}>
<Button variant={"primary"}>{l10n.getString("exposure-card-cta")}</Button>
</span>
);
const exposureCard = (
<div>
@ -307,7 +176,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
src={exposureImg}
/>
) : (
<>{fallbackLogo(props.exposureName)}</>
<FallbackLogo name={scanResult.data_broker} />
)
}
</dd>
@ -316,26 +185,30 @@ export const ExposureCard = (props: ExposureCardProps) => {
</dt>
<dd>
<span className={styles.exposureCompanyTitle}>
{exposureName}
{scanResult.data_broker}
</span>
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-exposure-type")}
</dt>
<dd className={styles.hideOnMobile}>
<ExposureTypeEl type={exposureData} />
{l10n.getString("exposure-card-exposure-type-data-broker")}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-date-found")}
</dt>
<dd className={styles.hideOnMobile}>
{dateFormatter.format(props.dateFound)}
{dateFormatter.format(
// We should be able to result that OneRep's `created_at` property is a properly-formatted data string
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
parseIso8601Datetime(scanResult.created_at)!
)}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-label-status")}
</dt>
<dd>
<StatusPill type={statusPillType} />
<StatusPill exposure={scanResult} />
</dd>
</dl>
<button
@ -367,79 +240,42 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCardExpanded ? styles.isOpen : ""
}`}
>
{isScanResult(exposureData) ? (
// Data broker content
<div>
<p>
{l10n.getFragment(
"exposure-card-description-info-for-sale-part-one",
{
elems: {
data_broker_link: <a href={exposureDetailsLink} />,
},
}
)}
<a href={exposureDetailsLink}>
<span>
<OpenInNew
alt={l10n.getString("open-in-new-tab-alt")}
width="13"
height="13"
/>
</span>
</a>
{l10n.getString(
"exposure-card-description-info-for-sale-part-two"
)}
</p>
</div>
) : (
// Data breach content
<div>
<p>
{l10n.getFragment(
"exposure-card-description-data-breach-part-one",
{
vars: {
data_breach_company: exposureName,
data_breach_date: exposureData.breachDate,
},
elems: {
data_breach_link: <a href={exposureDetailsLink} />,
},
}
)}
<a href={exposureDetailsLink}>
<span>
<OpenInNew
alt={l10n.getString("open-in-new-tab-alt")}
width="13"
height="13"
/>
</span>
</a>
{l10n.getString(
"exposure-card-description-data-breach-part-two"
)}
</p>
</div>
)}
<div>
<p>
{l10n.getFragment(
"exposure-card-description-info-for-sale-part-one",
{
elems: {
data_broker_link: <a href={scanResult.link} />,
},
}
)}
<a href={scanResult.link}>
<span>
<OpenInNew
alt={l10n.getString("open-in-new-tab-alt")}
width="13"
height="13"
/>
</span>
</a>
{l10n.getString(
"exposure-card-description-info-for-sale-part-two"
)}
</p>
</div>
<div className={styles.exposedInfoContainer}>
<div className={styles.exposedInfoWrapper}>
<p className={styles.exposedInfoTitle}>
{l10n.getString("exposure-card-your-exposed-info")}
</p>
<dl className={styles.dataClassesList}>
<ExposureCategoriesListElem />
{exposureCategoriesArray.map((item) => (
<React.Fragment key={item.key}>{item}</React.Fragment>
))}
</dl>
</div>
{(isScanResult(props.exposureData) &&
featureFlagsEnabled.PremiumBrokerRemoval &&
props.exposureData.status === "new") ||
(!isScanResult(props.exposureData) &&
// TODO: Add unit test when changing this code:
/* c8 ignore next 3 */
!props.exposureData.isResolved)
{isPremiumBrokerRemovalEnabled && props.scanResult.status === "new"
? letsFixItBtn
: null}
</div>
@ -450,3 +286,246 @@ export const ExposureCard = (props: ExposureCardProps) => {
return exposureCard;
};
export type SubscriberBreachCardProps = {
exposureImg?: StaticImageData;
subscriberBreach: SubscriberBreach;
locale: string;
};
const SubscriberBreachCard = (props: SubscriberBreachCardProps) => {
const { exposureImg, subscriberBreach, locale } = props;
const l10n = useL10n();
const [exposureCardExpanded, setExposureCardExpanded] = useState(false);
const dateFormatter = new Intl.DateTimeFormat(locale, {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle
dateStyle: "medium",
});
const exposureCategoriesArray: React.ReactElement[] = [];
const BreachExposureCategory = (props: BreachExposureCategoryProps) => {
const emailsList = (
<ul className={styles.emailsList}>
{props.emails?.map((email: string, index: number) => (
<li key={index}>{email}</li>
))}
</ul>
);
return (
<div className={styles.detailsFoundItem}>
<dt>
<span className={styles.exposureTypeIcon}>{props.icon}</span>
{l10n.getString("exposure-card-label-and-count", {
category_label: props.exposureCategoryLabel,
count: props.count,
})}
</dt>
<dd>{props.emails && emailsList}</dd>
</div>
);
};
subscriberBreach.dataClassesEffected.map((item: DataClassEffected) => {
const dataClass = Object.keys(item)[0];
const value = item[dataClass];
const emails = Array.isArray(value) ? value : [];
const count = typeof value === "number" ? value : 0;
if (dataClass === "email-addresses") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<EmailIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-email")}
emails={emails} // Only emails get listed
count={emails.length}
/>
);
} else if (dataClass === "passwords") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<PasswordIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-password")}
count={count}
/>
);
} else if (dataClass === "phone-numbers") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<PhoneIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-phone-number")}
count={count}
/>
);
} else if (dataClass === "ip-addresses") {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-ip-address")}
count={count}
/>
);
// TODO: Add unit test when changing this code:
/* c8 ignore next 12 */
}
// Handle all other breach categories
else {
exposureCategoriesArray.push(
<BreachExposureCategory
key={dataClass}
icon={<QuestionMarkCircle alt="" width="13" height="13" />} // default icon for categories without a unique one
exposureCategoryLabel={l10n.getString(dataClass)} // categories are localized in data-classes.ftl
count={count}
/>
);
}
});
const letsFixItBtn = (
<span className={styles.fixItBtn}>
<Button variant="primary" wide>
{l10n.getString("exposure-card-cta")}
</Button>
</span>
);
const exposureCard = (
<div>
<div className={styles.exposureCard}>
<div className={styles.exposureHeader}>
<dl className={styles.exposureHeaderList}>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-label-company-logo")}
</dt>
<dd
className={`${styles.hideOnMobile} ${styles.exposureImageWrapper}`}
>
{/* While logo is not yet set, the fallback image is the first character of the exposure name */}
{
// TODO: Add unit test when changing this code:
/* c8 ignore next 7 */
exposureImg ? (
<Image
className={styles.exposureImage}
alt=""
src={exposureImg}
/>
) : (
<FallbackLogo name={subscriberBreach.title} />
)
}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-label-company")}
</dt>
<dd>
<span className={styles.exposureCompanyTitle}>
{subscriberBreach.title}
</span>
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-exposure-type")}
</dt>
<dd className={styles.hideOnMobile}>
{l10n.getString("exposure-card-exposure-type-data-breach")}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-date-found")}
</dt>
<dd className={styles.hideOnMobile}>
{dateFormatter.format(subscriberBreach.addedDate)}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-label-status")}
</dt>
<dd>
<StatusPill exposure={subscriberBreach} />
</dd>
</dl>
<button
className={styles.chevron}
// TODO: Add unit test when changing this code:
/* c8 ignore next */
onClick={() => setExposureCardExpanded(!exposureCardExpanded)}
>
<ChevronDown
// TODO: Add unit test when changing this code:
/* c8 ignore next */
className={exposureCardExpanded ? styles.isOpen : ""}
alt={
// TODO: Add unit test when changing this code:
/* c8 ignore next 2 */
exposureCardExpanded
? l10n.getString("chevron-up-alt")
: l10n.getString("chevron-down-alt")
}
width="20"
height="20"
/>
</button>
</div>
<div
className={`${styles.exposureDetailsSection} ${
// TODO: Add unit test when changing this code:
/* c8 ignore next */
exposureCardExpanded ? styles.isOpen : ""
}`}
>
<div>
<p>
{l10n.getFragment(
"exposure-card-description-data-breach-part-one",
{
vars: {
data_breach_company: subscriberBreach.title,
data_breach_date: subscriberBreach.breachDate,
},
elems: {
data_breach_link: (
<Link href={`/breach-details/${subscriberBreach.name}`} />
),
},
}
)}
<Link href={`/breach-details/${subscriberBreach.name}`}>
<span>
<OpenInNew
alt={l10n.getString("open-in-new-tab-alt")}
width="13"
height="13"
/>
</span>
</Link>
{l10n.getString("exposure-card-description-data-breach-part-two")}
</p>
</div>
<div className={styles.exposedInfoContainer}>
<div className={styles.exposedInfoWrapper}>
<p className={styles.exposedInfoTitle}>
{l10n.getString("exposure-card-your-exposed-info")}
</p>
<dl className={styles.dataClassesList}>
{exposureCategoriesArray.map((item) => (
<React.Fragment key={item.key}>{item}</React.Fragment>
))}
</dl>
</div>
{
// TODO: Add unit test when changing this code:
/* c8 ignore next */
!props.subscriberBreach.isResolved ? letsFixItBtn : null
}
</div>
</div>
</div>
</div>
);
return exposureCard;
};

Просмотреть файл

@ -1,7 +1,7 @@
@import "../../tokens";
.stepsWrapper {
margin-block-end: $spacing-2xl;
margin-block-end: $layout-lg;
display: flex;
align-items: center;
justify-content: center;
@ -22,18 +22,63 @@
@media screen and (min-width: $screen-md) {
.steps {
$stepGap: $spacing-xl;
$stepGap: $layout-xl;
$iconDiameter: 40px;
$checkIconDiameter: 15px;
$iconBorderWidth: 4px;
$connectingLineHeight: 4px;
padding-inline-start: 0;
list-style-type: none;
display: flex;
gap: $stepGap;
max-width: $content-md;
position: relative;
max-width: $content-md;
align-items: center;
.progressBarLineContainer {
position: absolute;
width: 100%;
left: 0;
right: 0;
margin: auto;
.progressBarLineWrapper {
width: 100%;
height: 3px;
background-color: $color-grey-30;
position: relative;
.activeProgressBarLine {
background-color: $color-purple-10;
height: 3px;
position: absolute;
// These are not arbitrary "magic numbers." The width represents an increment of 100/8 due to the progress bar's ratio.
// 16.6667 is the closest round approximation of 100/8.
&.duringDataBrokerProfiles {
width: calc(16.6667% + $iconDiameter);
}
&.beginHighRiskDataBreaches {
width: 33.3333%;
}
&.duringHighRiskDataBreaches {
width: 50%;
}
&.beginLeakedPasswords {
width: 66.6667%;
}
&.duringLeakedPasswords {
width: calc(87.5% - $iconDiameter);
}
&.beginSecurityRecommendations {
width: 100%;
}
}
}
}
li {
$iconDiameter: 27px;
$iconBorderWidth: 4px;
$connectingLineHeight: 4px;
display: flex;
flex-direction: column;
align-items: center;
@ -42,65 +87,53 @@
font: $text-body-xs;
font-weight: 400;
color: $color-grey-30;
width: $layout-xl;
img {
background-color: $color-grey-30;
padding: 5px;
margin-block: $iconBorderWidth;
width: $iconDiameter;
height: $iconDiameter;
border-radius: 100%;
box-sizing: content-box;
pointer-events: none;
user-select: none;
// Overlap the `::before` (the horizontal line)
&.navigationItem {
// Overlap the horizontal line
z-index: 1;
}
// This is the horizontal line preceding every step indicator on desktop.
// It starts with `+ 2` because the lines are only between indicators, so
// the first one doesn't have a line preceding it, and nth-child is
// 1-indexed, i.e. it doesn't start at 0.
&:nth-child(n + 2)::before {
content: "";
background-color: $color-grey-30;
width: $layout-xl;
display: block;
height: $connectingLineHeight;
position: absolute;
top: calc(
($iconDiameter + $connectingLineHeight) / 2 + $iconBorderWidth
);
}
// TODO: Update logic to calculate this programmatically without the magic numbers
// MAGIC NUMBERS ALL THE WAY DOWN
&:nth-child(2)::before {
left: 13%;
}
&:nth-child(3)::before {
left: 40%;
}
&:nth-child(4)::before {
left: 67%;
.stepIcon {
width: $iconDiameter;
height: $iconDiameter;
margin-block: $iconBorderWidth;
pointer-events: none;
user-select: none;
box-sizing: border-box;
& > * {
width: 100%;
height: 100%;
border-radius: 100%;
padding: $spacing-sm;
background-color: $color-grey-30;
&.checkIcon {
color: $color-white;
padding: 12px; // Specific padding for check mark icon
}
}
}
&[aria-current="step"] {
img {
margin-block: 0;
border: $iconBorderWidth solid $color-purple-10;
}
&[aria-current="step"] .stepIcon > * {
outline: $iconBorderWidth solid $color-purple-10;
}
.stepLabel {
padding-top: $layout-md;
position: absolute;
width: 100px;
}
&[aria-current="step"] .stepLabel {
padding-top: calc($layout-md + $spacing-sm);
}
&.isCompleted,
&[aria-current="step"] {
color: $color-purple-70;
&::before {
background-color: $color-purple-10;
}
img {
.stepIcon > * {
background-color: $color-purple-70;
}
}
@ -116,7 +149,7 @@
gap: $spacing-sm;
li {
img {
.stepIcon {
// On mobile, the actual step images aren't actually shown; instead, we
// display flat horizontal lines, which we simulate by giving the images
// a 0 height and a border. The images are <1kB in size, so that
@ -130,11 +163,11 @@
user-select: none;
}
&[aria-current="step"] img {
&[aria-current="step"] .stepIcon {
border-color: $color-purple-10;
}
&.isCompleted img {
&.isCompleted .stepIcon {
border-color: $color-purple-70;
}
@ -144,6 +177,7 @@
color: $color-purple-70;
font: $text-body-xs;
font-weight: 600;
top: $spacing-lg;
}
&:not([aria-current="step"]) .stepLabel {

Просмотреть файл

@ -5,7 +5,6 @@
"use client";
import Image from "next/image";
import { usePathname } from "next/navigation";
import styles from "./FixNavigation.module.scss";
import { useState } from "react";
import { useL10n } from "../../hooks/l10n";
@ -18,6 +17,7 @@ type StepId =
export type Props = {
navigationItems: Array<NavigationItem>;
pathname: string;
};
export const FixNavigation = (props: Props) => {
@ -35,6 +35,7 @@ export const FixNavigation = (props: Props) => {
navigationItems={props.navigationItems}
showDataBrokerProfiles={showDataBrokerProfiles}
currentStep={currentStep}
pathname={props.pathname}
/>
</nav>
);
@ -44,7 +45,7 @@ export interface NavigationItem {
key: string;
labelStringId: string;
href: string;
status: string;
status: string | number;
currentStepId: string;
imageId: string;
}
@ -53,10 +54,33 @@ export const Steps = (props: {
showDataBrokerProfiles: boolean;
currentStep: StepId;
navigationItems: Array<NavigationItem>;
pathname: string;
}) => {
const l10n = useL10n();
const pathname = usePathname();
function calculateActiveProgressBarPosition(pathname: string) {
if (pathname === "/redesign/user/dashboard/fix/high-risk-data-breaches") {
return styles.beginHighRiskDataBreaches;
} else if (
pathname.startsWith(
"/redesign/user/dashboard/fix/high-risk-data-breaches"
)
) {
return styles.duringHighRiskDataBreaches;
} else if (pathname === "/redesign/user/dashboard/fix/leaked-passwords") {
return styles.beginLeakedPasswords;
} else if (
pathname.startsWith("/redesign/user/dashboard/fix/leaked-passwords")
) {
return styles.duringLeakedPasswords;
} else if (
pathname === "/redesign/user/dashboard/fix/security-recommendations"
) {
return styles.beginSecurityRecommendations;
} else {
return "";
}
}
return (
<ul className={styles.steps}>
@ -64,24 +88,32 @@ export const Steps = (props: {
({ key, labelStringId, href, imageId, status }) => (
<li
key={key}
aria-current={pathname.includes(href) ? "step" : undefined}
className={`${styles.fixNavigationItem} ${
pathname.includes(href) ? styles.active : ""
aria-current={props.pathname.includes(href) ? "step" : undefined}
className={`${styles.navigationItem} ${
props.pathname.includes(href) ? styles.active : ""
}`}
>
<Image
// TODO: Add "isDone" logic
src={imageId}
alt=""
width={22}
height={22}
/>
<div className={styles.stepIcon}>
<Image src={imageId} alt="" width={22} height={22} />
{/* // TODO: Add logic to mark icon as checked when step is complete */}
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
</div>
<div className={styles.stepLabel}>
{l10n.getString(labelStringId)} ({status})
</div>
</li>
)
)}
<li className={styles.progressBarLineContainer}>
<div className={styles.progressBarLineWrapper}>
<div
className={`${
styles.activeProgressBarLine
} ${calculateActiveProgressBarPosition(props.pathname)}`}
></div>
</div>
</li>
</ul>
);
};

Просмотреть файл

@ -16,7 +16,7 @@ import ShieldIcon from "./assets/shield-icon.svg";
import styles from "./PremiumBadge.module.scss";
export type Props = {
user: Session["user"] | null;
user: Session["user"];
};
export default function PremiumBadge({ user }: Props) {

Просмотреть файл

@ -80,11 +80,11 @@ function PremiumUpsellDialogContent() {
},
];
const premiumSubscriptionUrl =
process.env.FXA_SUBSCRIPTIONS_URL &&
process.env.PREMIUM_PRODUCT_ID &&
process.env.PREMIUM_PLAN_ID_US &&
`${process.env.FXA_SUBSCRIPTIONS_URL}/products/${process.env.PREMIUM_PRODUCT_ID}?plan=${process.env.PREMIUM_PLAN_ID_US}`;
const premiumSubscriptionUrl = `${
process.env.FXA_SUBSCRIPTIONS_URL as string
}/products/${process.env.PREMIUM_PRODUCT_ID as string}?plan=${
process.env.PREMIUM_PLAN_ID_US as string
}`;
return (
<div className={styles.modalContent}>

Просмотреть файл

@ -1,3 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 19C7.58172 19 4 15.4183 4 11C4 6.58172 7.58172 3 12 3C16.4183 3 20 6.58172 20 11C19.9952 15.4163 16.4163 18.9952 12 19ZM16.5324 12.7674C15.3474 11.3999 13.7066 13.24 11.9998 13.24C10.294 13.24 8.65207 11.3999 7.46711 12.7674C7.13111 13.1549 7.10423 13.7194 7.39207 14.1427C8.39671 15.6211 10.0767 16.6 11.9998 16.6C13.9228 16.6 15.6028 15.6211 16.6074 14.1427C16.8964 13.7194 16.8684 13.1549 16.5324 12.7674V12.7674ZM12 12.2C13.8779 12.2 15.4 10.6779 15.4 8.79999C15.4 6.92206 13.8779 5.39999 12 5.39999C10.122 5.39999 8.59998 6.92206 8.59998 8.79999C8.59998 10.6779 10.122 12.2 12 12.2Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 20C6.02943 20 2 15.9706 2 11C2 6.02943 6.02943 2 11 2C15.9706 2 20 6.02943 20 11C19.9946 15.9683 15.9683 19.9946 11 20ZM16.099 12.9883C14.7658 11.4499 12.9199 13.52 10.9998 13.52C9.08075 13.52 7.23358 11.4499 5.9005 12.9883C5.5225 13.4243 5.49226 14.0593 5.81608 14.5355C6.9463 16.1987 8.83629 17.3 10.9998 17.3C13.1631 17.3 15.0531 16.1987 16.1833 14.5355C16.5084 14.0593 16.477 13.4243 16.099 12.9883ZM11 12.35C13.1126 12.35 14.825 10.6376 14.825 8.52499C14.825 6.41232 13.1126 4.69999 11 4.69999C8.88725 4.69999 7.17498 6.41232 7.17498 8.52499C7.17498 10.6376 8.88725 12.35 11 12.35Z" fill="white"/>
</svg>

До

Ширина:  |  Высота:  |  Размер: 760 B

После

Ширина:  |  Высота:  |  Размер: 759 B

Просмотреть файл

@ -26,21 +26,13 @@ const BreachMockItem = createRandomBreach();
export const DataBroker: Story = {
args: {
exposureImg: FamilyTreeImage,
exposureName: ScanMockItem.data_broker,
exposureData: ScanMockItem,
exposureDetailsLink: "linkehere.com",
dateFound: new Date(ScanMockItem.created_at),
statusPillType: "needAction",
},
};
export const DataBreach: Story = {
args: {
exposureImg: TwitterImage,
exposureName: "Twitter",
exposureData: BreachMockItem,
exposureDetailsLink: "linkehere.com",
dateFound: new Date(BreachMockItem.addedDate),
statusPillType: "needAction",
},
};

Просмотреть файл

@ -10,7 +10,7 @@ import { AppPicker } from "./AppPicker";
import PremiumBadge from "../../client/PremiumBadge";
export type Props = {
user: Session["user"] | null;
user: Session["user"];
children?: ReactNode;
};

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше