1 import { useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { useUser } from '@proton/account/user/hooks';
6 import { useUserSettings } from '@proton/account/userSettings/hooks';
7 import { Href } from '@proton/atoms';
8 import Icon from '@proton/components/components/icon/Icon';
9 import Loader from '@proton/components/components/loader/Loader';
10 import { useModalStateObject } from '@proton/components/components/modalTwo/useModalState';
11 import Toggle from '@proton/components/components/toggle/Toggle';
12 import SettingsLayout from '@proton/components/containers/account/SettingsLayout';
13 import SettingsLayoutLeft from '@proton/components/containers/account/SettingsLayoutLeft';
14 import SettingsLayoutRight from '@proton/components/containers/account/SettingsLayoutRight';
15 import SettingsParagraph from '@proton/components/containers/account/SettingsParagraph';
16 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
17 import GenericError from '@proton/components/containers/error/GenericError';
18 import { useSubscriptionModal } from '@proton/components/containers/payments/subscription/SubscriptionModalProvider';
19 import { SUBSCRIPTION_STEPS } from '@proton/components/containers/payments/subscription/constants';
20 import useActiveBreakpoint from '@proton/components/hooks/useActiveBreakpoint';
21 import useApi from '@proton/components/hooks/useApi';
22 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
23 import useNotifications from '@proton/components/hooks/useNotifications';
24 import { useLoading } from '@proton/hooks';
25 import { getBreaches, updateBreachEmailNotificationsState, updateBreachState } from '@proton/shared/lib/api/breaches';
26 import { getApiError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
27 import { disableBreachAlert, enableBreachAlert } from '@proton/shared/lib/api/settings';
31 DARK_WEB_MONITORING_NAME,
34 } from '@proton/shared/lib/constants';
35 import { getUpsellRef } from '@proton/shared/lib/helpers/upsell';
36 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
38 DARK_WEB_MONITORING_ELIGIBILITY_STATE,
39 DARK_WEB_MONITORING_EMAILS_STATE,
40 DARK_WEB_MONITORING_STATE,
41 } from '@proton/shared/lib/interfaces';
42 import freeUserBreachImg from '@proton/styles/assets/img/breach-alert/img-breaches-found.svg';
43 import freeUserNoBreachImg from '@proton/styles/assets/img/breach-alert/img-no-breaches-found-inactive.svg';
44 import useFlag from '@proton/unleash/useFlag';
45 import clsx from '@proton/utils/clsx';
46 import noop from '@proton/utils/noop';
48 import BreachEmailToggle from './BreachEmailToggle';
49 import BreachInformationCard from './BreachInformationCard';
50 import BreachModal from './BreachModal';
51 import BreachMonitoringToggle from './BreachMonitoringToggle';
52 import BreachesList from './BreachesList';
53 import EmptyBreachListCard from './EmptyBreachListCard';
54 import NoBreachesView from './NoBreachesView';
55 import { BREACH_API_ERROR, getEnableString, getUpsellText, toCamelCase } from './helpers';
56 import type { ListType, SampleBreach } from './models';
57 import { BREACH_STATE } from './models';
58 import { useBreaches } from './useBreaches';
60 const LIST_STATES_MAP: Record<ListType, BREACH_STATE[]> = {
61 open: [BREACH_STATE.UNREAD, BREACH_STATE.READ],
62 resolved: [BREACH_STATE.RESOLVED],
65 const CredentialLeakSection = () => {
66 const canDisplayDWMEmailToggle = useFlag('DarkWebEmailNotifications');
67 const handleError = useErrorHandler();
68 const [loading, withLoading] = useLoading();
69 const [breachesLoading] = useLoading();
70 const [toggleLoading, withToggleLoading] = useLoading();
71 const [actionLoading, withActionLoading] = useLoading();
72 const [emailToggleLoading, withEmailToggleLoading] = useLoading();
73 const [openSubscriptionModal] = useSubscriptionModal();
75 const [user] = useUser();
76 const [{ BreachAlerts }] = useUserSettings();
77 const { createNotification } = useNotifications();
78 const breachModal = useModalStateObject();
79 const { viewportWidth } = useActiveBreakpoint();
80 const { breaches: breachList, actions } = useBreaches();
82 const [selectedBreachID, setSelectedBreachID] = useState<string | null>(null);
83 const [listType, setListType] = useState<ListType>('open');
84 const [total, setTotal] = useState<number | null>(null);
85 const [error, setError] = useState<{ message: string } | null>(null);
86 const [openModal, setOpenModal] = useState<boolean>(false);
87 const [sample, setSample] = useState<SampleBreach | null>(null);
88 const [hasBeenInteractedWith, setHasBeenInteractedWith] = useState<boolean>(false);
90 // TODO: change nums to constants
91 const [hasAlertsEnabled, setHasAlertsEnabled] = useState<boolean>(
92 BreachAlerts.Eligible === DARK_WEB_MONITORING_ELIGIBILITY_STATE.PAID &&
93 BreachAlerts.Value === DARK_WEB_MONITORING_STATE.ENABLED
95 const [hasEmailsEnabled, setHasEmailsEnabled] = useState<boolean>(
96 BreachAlerts.EmailNotifications === DARK_WEB_MONITORING_EMAILS_STATE.ENABLED
98 const isPaidUser = user.isPaid;
105 const fetchLeakData = async () => {
107 const { Breaches, Samples, IsEligible, Count } = await api(getBreaches());
110 const breaches = toCamelCase(Breaches);
111 actions.load(breaches);
113 setSelectedBreachID(breaches[0].id);
116 const fetchedSample = toCamelCase(Samples);
117 setSample(fetchedSample[0]);
121 const { message, code } = getApiError(e);
122 if (code === BREACH_API_ERROR.GENERIC) {
123 setError({ message: message });
131 withLoading(fetchLeakData()).catch(noop);
132 }, [hasAlertsEnabled]);
135 const handleBreachModal = () => {
136 if (!loading && viewportWidth['<=medium'] && openModal) {
137 breachModal.openModal(true);
138 setOpenModal(!openModal);
142 }, [loading, openModal, viewportWidth]);
144 // TODO: if BE api returns read and opened breaches, can remove filter
145 const viewingBreachList = breachList.filter((breach) => LIST_STATES_MAP[listType].includes(breach.resolvedState));
146 const viewingBreach = viewingBreachList.find((b) => b.id === selectedBreachID) ?? viewingBreachList[0];
148 const isFirstItemUnread =
149 viewingBreachList.length > 0 && viewingBreachList[0].resolvedState === BREACH_STATE.UNREAD;
150 const firstBreach = viewingBreachList[0];
152 const markAsResolvedBreach = async () => {
153 if (!viewingBreach) {
157 await withActionLoading(
160 ID: viewingBreach.id,
161 State: BREACH_STATE.RESOLVED,
165 actions.resolve(viewingBreach);
166 setSelectedBreachID(null);
172 const markAsOpenBreach = async () => {
173 if (!viewingBreach) {
179 ID: viewingBreach.id,
180 State: BREACH_STATE.READ,
183 actions.open(viewingBreach);
189 const handleEnableBreachAlertToggle = async (newToggleState: boolean) => {
190 if (newToggleState === hasAlertsEnabled) {
194 const [action, notification] = newToggleState
195 ? [enableBreachAlert, c('Notification').t`Dark Web Monitoring has been enabled`]
196 : [disableBreachAlert, c('Notification').t`Dark Web Monitoring has been disabled`];
198 await withToggleLoading(api(action()));
199 createNotification({ text: notification });
200 setHasAlertsEnabled(newToggleState);
206 const handleEmailNotificationsToggle = async (newState: boolean) => {
207 const notification = newState
208 ? c('Notification').t`Email notifications have been enabled`
209 : c('Notification').t`Email notifications have been disabled`;
211 await withEmailToggleLoading(api(updateBreachEmailNotificationsState({ Enabled: newState })));
212 createNotification({ text: notification });
213 setHasEmailsEnabled(newState);
219 // need upsellRef to differentiate between breach alert upsells in account and inbox
220 const upsellRef = getUpsellRef({
221 app: APP_UPSELL_REF_PATH.ACCOUNT_UPSELL_REF_PATH,
222 component: UPSELL_COMPONENT.TOGGLE,
223 feature: MAIL_UPSELL_PATHS.BREACH_ALERTS,
226 const handleUpgrade = () => {
227 openSubscriptionModal({
228 step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
230 mode: 'upsell-modal',
232 onSubscribed: () => {
233 handleEnableBreachAlertToggle(true);
240 if (viewingBreach === firstBreach && isFirstItemUnread && hasBeenInteractedWith) {
243 if (viewingBreach !== firstBreach && viewingBreach.resolvedState === BREACH_STATE.UNREAD) {
247 }, [viewingBreach, hasBeenInteractedWith]);
249 const href = getKnowledgeBaseUrl('/dark-web-monitoring');
250 // translator: full sentence is: We monitor the dark web for instances where your personal information (such as an email address or password used on a third-party site) is leaked or compromised. <How does monitoring work?>
251 const dataBreachLink = (
252 <Href key={'breach'} className="inline-block" href={href}>{c('Link').t`How does monitoring work?`}</Href>
255 const breachAlertIntroText = (
258 // translator: full sentence is: We monitor the dark web for instances where your personal information (such as an email address or password used on a third-party site) is leaked or compromised. <How does monitoring work?>
260 .jt`We monitor the dark web for instances where your personal information (such as an email address or password used on a third-party site) is leaked or compromised.`
266 const breachAlertInfoSharing = (
269 .t`${BRAND_NAME} never shares your information with third parties. Data comes from ${BRAND_NAME}'s own analyses and Constella Intelligence.`}{' '}
271 // translator: full sentence is: Proton never shares your information with third parties. Data comes from Proton's own analyses and Constella Intelligence. Support for monitoring of custom domains and non-Proton email addresses is coming soon.
273 .t`Support for monitoring of custom domains and non-${BRAND_NAME} email addresses is coming soon.`
278 // translator: full sentence is: Get notified if your password or other personal data was leaked. <Learn more>
279 const learnMoreLinkNoBreach = <Href href={href} className="inline-block">{c('Link').t`Learn more`}</Href>;
281 // translator: full sentence is: Your information was found in at least one data breach. Turn on Dark Web Monitoring to view details and take action. <Learn more>
282 const learnMoreLinkBreach = (
283 <Href href={href} className="inline-block color-danger">{c('Link').t`Learn more`}</Href>
288 <SettingsSectionWide>
291 return <GenericError className="text-center">{error.message}</GenericError>;
294 return <Loader size="medium" />;
299 <div className="flex flex-nowrap">
300 <div className="flex-1">
301 {breachAlertIntroText}
305 .jt`Get notified if your password or other personal data was leaked. ${learnMoreLinkNoBreach}`}
310 className="flex flex-nowrap color-danger p-4 rounded"
311 style={{ 'background-color': 'var(--signal-danger-minor-2)' }}
314 name="exclamation-circle-filled"
315 className="shrink-0 mt-0.5 mr-2"
317 <span className="flex-1">
318 {getUpsellText(sample, total, learnMoreLinkBreach, true)}
326 <label className="text-semibold" htmlFor="data-breach-toggle">
327 <span className="mr-2">
328 {getEnableString(DARK_WEB_MONITORING_NAME)}
331 </SettingsLayoutLeft>
332 <SettingsLayoutRight isToggleContainer>
334 id="data-breach-toggle"
337 onClick={handleUpgrade}
339 </SettingsLayoutRight>
343 <div className="hidden lg:flex">
345 src={total === 0 ? freeUserNoBreachImg : freeUserBreachImg}
357 {breachAlertIntroText}
358 {breachAlertInfoSharing}
359 <BreachMonitoringToggle
360 enabled={hasAlertsEnabled}
361 loading={toggleLoading}
362 onToggle={handleEnableBreachAlertToggle}
364 {canDisplayDWMEmailToggle && (
366 enabled={hasEmailsEnabled}
367 loading={emailToggleLoading}
368 onToggle={handleEmailNotificationsToggle}
376 className="flex flex-nowrap lg:flex-row w-full max-h-custom lg:max-h-custom"
377 style={{ '--max-h-custom': '40vh', '--lg-max-h-custom': '90vh' }}
381 selectedID={selectedBreachID}
383 setSelectedBreachID(id);
385 if (!hasBeenInteractedWith) {
386 setHasBeenInteractedWith(true);
389 isPaidUser={isPaidUser}
392 onViewTypeChange={setListType}
394 {viewingBreachList.length === 0 && (
397 'flex relative w-full md:w-2/3',
398 viewportWidth['<=medium'] && 'hidden'
401 <EmptyBreachListCard listType={listType} />
407 'relative w-full md:w-2/3',
408 viewportWidth['<=medium'] && 'hidden'
410 onMouseEnter={() => {
411 if (!hasBeenInteractedWith) {
412 setHasBeenInteractedWith(true);
416 <BreachInformationCard
417 breachData={viewingBreach}
418 onResolve={markAsResolvedBreach}
419 onOpen={() => withActionLoading(markAsOpenBreach())}
420 isMutating={breachesLoading}
421 loading={actionLoading}
430 </SettingsSectionWide>
431 {breachModal.render && (
433 modalProps={breachModal.modalProps}
434 breachData={viewingBreach}
437 actions.resolve(viewingBreach);
446 export default CredentialLeakSection;