Merge branch 'main' into MNTOR-2069-README
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
43
README.md
|
@ -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 that’s 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 = Here’s 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. It’s free, lasts one year, and won’t negatively affect your credit score.
|
||||
ssn-modal-description-fraud-part-two = To set one up, contact any one of the three credit bureaus. You don’t 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. It’s free and won’t negatively affect your credit score, but you’ll 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 = Here’s 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 can’t 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 = Don’t 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 can’t fix this. But there are steps you can take to protect yourself.
|
||||
security-recommendation-email-step-one = Don’t 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 you’ve used the same password. Change it anywhere you’ve used it to protect yourself.
|
||||
leaked-passwords-steps-title = Here’s what to do
|
||||
leaked-passwords-steps-subtitle = This requires access to your account, so you’ll 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 you’ve 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 you’ve used the same security questions. Update them now to protect your accounts.
|
||||
leaked-security-questions-steps-title = Here’s what to do
|
||||
leaked-security-questions-steps-subtitle = This requires access to your account, so you’ll 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 it’s been involved in a breach.
|
||||
*[other] Your account includes monitoring of up to { $total } email addresses. Add a new email address to see if it’s 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 you’ve been part of a data breach with { -brand-fx-monitor }. We’ll 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 = We’re sorry, the page you’re 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. Here’s 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 = We’ll 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 }. We’ll 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! You’ve resolved all breaches for { $email }. We’ll 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, we’d recommend that you change your password on the company’s 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 you’re using unique passwords for all accounts, so that any leaked passwords can’t 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 don’t 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. It’s free and won’t 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. You’ll 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, doesn’t 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, we’d recommend that you update your security questions on the company’s 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 you’ve 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 you’ve 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. We’ll 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. We’ll 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 you’ve 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
|
|||
|
||||
## Here’s how it works
|
||||
|
||||
how-it-works = Here’s how it works
|
||||
check-for-breaches = Check for breaches
|
||||
check-for-breaches-we-search = We’ll search all known data breaches since 2007 to see if your info was compromised.
|
||||
protect-accounts = Protect your accounts
|
||||
protect-accounts-clear-steps = We’ll give you clear steps on what to do next for any data breach you’ve been involved in.
|
||||
alerts-for-breaches = Get alerts for new breaches
|
||||
alerts-for-breaches-monitor-new = We’ll continually monitor for new data breaches and let you know if you’ve 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. We’re 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 people’s personal information resides. A data breach can also happen by accident like if someone’s login credentials accidentally get posted publicly.
|
||||
# question and answer
|
||||
what-do-i-do = I just found out I’m 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 it’s 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 you’ve been in a data breach
|
||||
|
||||
see-if-data-breach = See if you’ve 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 user’s 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 you’ll 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 it’s 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 = We’re sorry to see you go. <br /> Will you tell us why you’re 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 mba’ete?
|
||||
fxa-pwt-summary-2 =
|
||||
Ñe’ẽñemi mbykýva peteĩ ñe’ẽnteva ndahasýi ijeikuaa mba’evai apohápe g̃uarã.
|
||||
Eipuru sa’ivéramo mokõi ñe’ẽ ha embojopyru tai, papapy ha ambueve.
|
||||
Eiporu sa’ivé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ĩ mba’ete eikuaa’ỹva? Ne mba’ekuaarã ikatu oñevendémara’e
|
||||
térã oñemyasãima. Kóva avei ikatu ha’e mba’ete nderesaráiva emoheñói hag̃ua térã peteĩ mba’apohaguasu 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 mba’evaiapoha tembiporupyahugua oipurujeykuaa ñe’ẽñemi ojehechakuaáva eike hag̃ua ambue mba’etépe. Emoheñói ñe’ẽñemi pyahu ha ha ha’eñóva peteĩteĩva mba’etépe, ejapokuaáva ne mba’ete 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 mba’evaiapoha tembiporupyahugua oiporujeykuaa ñe’ẽñemi ojehechakuaáva eike hag̃ua ambue mba’etépe. Emoheñói ñe’ẽñemi pyahu ha ha ha’eñóva peteĩteĩva mba’etépe, ejapokuaáva ne mba’ete banco pegua, ne ñanduti veve ha ambue ñanduti renda eñongatu hague ne maranduete.
|
||||
fxa-what-to-do-blurb-3 =
|
||||
Heta mba’evai ohechauka ñanduti veve ha ñe’ẽñemi año, hákatu oĩ avei omoinge marandu virugua ñemiguáva.
|
||||
Ne mba’ete banco térã kuatia’atã ñemurã papapy ojehechakuaa kuri, emombe’u ne báncope ikatuha oiko mba’evai.
|
||||
Ehechajey umi je’epyre eikuaa’ỹva.
|
||||
fxa-what-to-do-subhead-4 = Eñepytyvõuka nemandu’a 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 mandu’apá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 mandu’apá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 mba’ekuaarã mbyatyha omono’õ marandu opavave mba’évagui
|
||||
ha avei ojogua ambue mba’apohaguasúgui. Ombojuaju marandu ovende hag̃ua ambue
|
||||
mba’apohaguasúpe jekuaaukarãve. Umi ohupytýva ko’ã ñembyai ndorekói
|
||||
araka’eve viru ñemonda, hákatu umi hekovaíva ñadutípe ikatu oipuru pe marandu omyendague hag̃ua chupe.
|
||||
araka’eve 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 emoambueva’erã.
|
||||
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 mba’ete itujáva, embohekopyahúke ne ñe’ẽñemi.
|
|||
make-new-pw-unique = Ejapo ñe’ẽñemi pyahúgui iñambue ha ha’eñó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 ha’etéva ha eñongatu tenda hekorosãvape, peteĩ ñe’ẽñemi ñangarekohápe.
|
||||
five-myths = 5 mombe’ugua’u ñ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 tai’i ha taiguasu,
|
||||
papapy ha ambueve. Ndoguerekói marandu mba’eteé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 mba’etépe. Emo’ãporãve hag̃ua
|
||||
ha oiporu ñepyrũ hag̃ua tembiapo ambue ne mba’etépe. Emo’ãporãve hag̃ua
|
||||
eipurúke ñe’ẽñemi ha’eté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 ha’eñóva. Ko’ã ñangarekoha ombyaty opaite ne
|
||||
Oiporu ñe’ẽñemi ñangarekoha omoheñói hag̃ua ñe’ẽñemi hekorosãva ha ha’eñóva. Ko’ã ñangarekoha ombyaty opaite ne
|
||||
rembiapo ñepyrũ hekorosãva eike hag̃ua opaite ne mba’e’oka guive.
|
||||
faq1 = Ndaikuaái ko mba’apohaguasu térã ñanduti renda. ¿Mba’ére aime ko ñembyaípe?
|
||||
faq2 = ¿Mba’ére eha’arõ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 mba’evai apoha ñandutiguá
|
|||
what-to-do-after-breach-desc = Ejoko ne mba’ete 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 mba’etépa pe ehekava’erã.
|
||||
five-myths-desc = Eikuaa mb’aéichapa emboykéta ñe’ẽñemi jepuruvai nombohasýiva hekovaíva rembiapo.
|
||||
five-myths-desc = Eikuaa mb’aé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 = Emba’ejogua tovara’ãnga reheve ñanduti veve rupive
|
||||
ad-unit-4-want-to-buy = Emba’ejoguasépa ñandutí rupive ha ndereikuaái jejoguaha térã nderejeroviái hese
|
||||
ad-unit-4-shop-online = Eipuru ñanduti veve rovara’ãnga emba’ejoguávo ñanduti rupive. Erekóta ñemoneĩ ne ñanduti vevépe ha upéi eipe’a tovara’ãnga ejapose vove.
|
||||
ad-unit-4-shop-online = Eiporu ñanduti veve rovara’ãnga emba’ejoguávo ñanduti rupive. Erekóta ñemoneĩ ne ñanduti vevépe ha upéi eipe’a tovara’ãnga ejapose vove.
|
||||
# ad 5 heading
|
||||
ad-unit-5-on-the-go = Pya’eterei { -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 pya’e 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 nemba’eteéva kuatia ñemba’ejoguakue 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 nemba’eteéva sapy’aité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ã mba’ekuaarã ñ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 } Mba’ekuaarã
|
|||
# Variables:
|
||||
# $company (String) - Name of the company that was breached, e.g. "PHP Freaks"
|
||||
breach-detail-meta-social-title = ¿Ne myangekói { $company } mba’ekuaarã ñembogua?
|
||||
breach-detail-meta-social-description = Eipuru { -brand-fx-monitor } eikuaa hag̃ua ne maranduetépa oñemboguakuaára’e ha péicha rupi eikuaa mba’etépa ejapóta.
|
||||
breach-detail-meta-social-description = Eiporu { -brand-fx-monitor } eikuaa hag̃ua ne maranduetépa oñemboguakuaára’e ha péicha rupi eikuaa mba’eté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 mba’ete reko banco pegua, virujepuru térã kuatia’atã ñemurã emoneĩ’ỹva.
|
||||
breach-checklist-ssn-header = Ehechameme ne mba’ete reko banco pegua, virujeporu térã kuatia’atã ñ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 kuatia’atã ñemurã reko eikuaa
|
|||
|
||||
## Prompts the user for changes when there is a breach detected of bank account
|
||||
|
||||
breach-checklist-bank-header = Emomarandu pya’e banco-pe ne mba’ete papapy ojepurukuaátaramo.
|
||||
breach-checklist-bank-header = Emomarandu pya’e banco-pe ne mba’ete papapy ojeporukuaátaramo.
|
||||
breach-checklist-bank-body = Ejapo pya’evéramo ikatu ne mo’ãve nepytyvõkuaáva oimeraẽva mba’evaígui. Avei ehechajeykuaa ne mba’ete ehekahápe oimeraẽva mba’e nemba’e’ỹ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 mba’epuru 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 mba’ete hekorosã ha ha’eñóva oimeraẽva mbaétépe g̃uarã eipurujeyhague ñe’ẽñemi.
|
||||
breach-checklist-hp-header = Emoheñói mba’ete hekorosã ha ha’eñó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 mba’e’oka 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 = Mba’e’oka marandu
|
||||
device-serial-numbers = Mba’e’oka papapy hekoetáva
|
||||
device-usage-tracking-data = Mba’e’oka jepuru mba’ekuaarã rapykuehoha
|
||||
device-usage-tracking-data = Mba’e’oka jeporu mba’ekuaarã rapykuehoha
|
||||
drinking-habits = Guari ñemokõ meme
|
||||
driver-s-licenses = Mongu’ekuaa ñe’ẽme’ẽguigua
|
||||
drug-habits = Droga je’u meme
|
||||
|
@ -55,7 +55,7 @@ ethnicities = Atygua mba’ekuaarã
|
|||
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 = Teteku’e 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 = Mba’etee 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ára’e
|
||||
exposure-landing-hero-lead = Eñemohekorosã { -brand-firefox } rembiporu ñemigua moheñoihára ndive ne mo’ãtava mba’evaiapohágui ha mba’apohaguasu omoherakuã ha ovendéva ne reheguaite. Romomarandúta mba’ekuaarã ñ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 jeku’erã 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 mba’evai 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 marandu’i viruguáva
|
|||
rec-ssn =
|
||||
Eguereko mbohapy marandu’i ñemurã reigua arýpe léipe he’iháicha.
|
||||
ejerure ha ehechajey nombyaíri ne ñemurã.
|
||||
Eheka mba’ete, virujepuru térã kuatia’atã ñemurã eikuaa’ỹva.
|
||||
Eheka mba’ete, virujeporu térã kuatia’atã ñ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 mba’ekuaarã ñembyai. Kóva oikóvo
|
||||
ñe’ẽñemi eiporukuaa, umi mba’evai apoha ikatu oiporu oike hag̃ua ambue mba’etépe.
|
||||
# Recommendation subhead
|
||||
rec-pw-3-subhead = 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ñongatuva’ekue
|
||||
Eiporu { -brand-lockwise } eike hag̃ua tekorosãme ñe’ẽñemi eñongatuva’ekue
|
||||
{ -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 kuatia’atã ñemurã. Ikatu hína
|
||||
ejerure kuatia’atã ñ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 ha’etéva remoĩma umi mba’evaiapoha ha tapykuehoha
|
||||
ojuhúvo ne ñe’ẽñemi térã ndejuhúvo ñandutípe. Ko { -brand-relay } rembiapo
|
||||
ha’e omokañývo ne ñanduti veve emondojey aja ñandutiveve ne g̃uahẽhame.
|
||||
# Recommendation subhead
|
||||
rec-ip-subhead-2 = 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 mba’epuru { -brand-mozilla-vpn } rehegua
|
||||
omoypytũ nde IP kundaharape oñomi hag̃ua ne rendaite.
|
||||
rec-hist-pw-subhead = Aníke eipurujo’a ñe’ẽñemi
|
||||
rec-hist-pw-subhead = Aníke eiporujo’a ñe’ẽñemi
|
||||
# Link title
|
||||
rec-hist-pw-cta-fx = Ehecha tembiapo ñepyrũ { -brand-name }
|
||||
rec-hist-pw = Eipuru ñe’ẽñemi ha’ete ha hekorosãva peteĩteĩva mba’etépe g̃uarã. Pe ñe’ẽñemi oreko ijehe mba’ekuaarã ñembyai, tekotevẽ embohekopyahu tembiapo ñepyrũ año.
|
||||
rec-hist-pw = Eiporu ñe’ẽñemi ha’ete ha hekorosãva peteĩteĩva mba’etépe g̃uarã. Pe ñe’ẽñemi oreko ijehe mba’ekuaarã ñembyai, tekotevẽ embohekopyahu tembiapo ñepyrũ año.
|
||||
# Recommendation subhead
|
||||
rec-sec-qa-subhead = Emoheñói mbohovái porandu hekorosãva rehegua
|
||||
rec-sec-qa =
|
||||
|
@ -79,25 +79,25 @@ rec-phone-num =
|
|||
Ani eme’ẽ ne pumbyry papapy eñemboheraguapývo mba’ete
|
||||
ipyahúvape térã mba’epururã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. Orekova’erã 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. Mba’ekuaarã ndahasýiva ijuhu oimehápe, péva rupi ko’ã
|
||||
ñe’ẽñemi ndahasýi ijekuaa.
|
||||
# Recommendation subhead
|
||||
rec-gen-1-subhead = Eipuru ñe’ẽñemi ha’eño ha hekorosãva peteĩteĩva mba’etépe g̃uarã
|
||||
rec-gen-1-subhead = Eiporu ñe’ẽñemi ha’eño ha hekorosãva peteĩteĩva mba’etépe g̃uarã
|
||||
# Link title
|
||||
rec-gen-1-cta = Mba’éicha emoheñóita ñe’ẽñemi hekorosãva
|
||||
rec-gen-1 = Ñe’ẽñemi eipurujeýrõ ombyaikuaa opaite ne mba’ete. Kóva he’ise pe ñe’ẽñemi ojehechakuaa, umi mba’evaiapoha oreko mba’eñemi heta mba’ete rehegua.
|
||||
rec-gen-1 = Ñe’ẽñemi eiporujeýrõ ombyaikuaa opaite ne mba’ete. Kóva he’ise pe ñe’ẽñemi ojehechakuaa, umi mba’evaiapoha oreko mba’eñemi heta mba’ete rehegua.
|
||||
# Recommendation subhead
|
||||
rec-gen-2-subhead = Embyaty ñe’ẽñemi tenda hekorosãvape
|
||||
# Link title
|
||||
|
|
|
@ -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 = 并非所有外泄事件都会泄露同样的信息,这具体取决于黑客取得了哪些信息。许多数据外泄会泄露电子邮件地址和密码,有些还会泄露更敏感的信息,例如信用卡号、护照号和社保号。
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 />,
|
||||
},
|
||||
})}
|
||||
|
||||
{l10n.getString("ssn-modal-description-fraud-part-two")}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getFragment(
|
||||
"ssn-modal-description-freeze-credit-part-one",
|
||||
{
|
||||
elems: {
|
||||
b: <strong />,
|
||||
},
|
||||
}
|
||||
)}
|
||||
|
||||
{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: "Here’s 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: "Here’s 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;
|
||||
};
|
||||
|
||||
|
|