1 import { useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { Href } from '@proton/atoms';
13 } from '@proton/components';
14 import Icon from '@proton/components/components/icon/Icon';
15 import Loader from '@proton/components/components/loader/Loader';
16 import { useModalStateObject } from '@proton/components/components/modalTwo/useModalState';
17 import Toggle from '@proton/components/components/toggle/Toggle';
18 import SettingsLayout from '@proton/components/containers/account/SettingsLayout';
19 import SettingsLayoutLeft from '@proton/components/containers/account/SettingsLayoutLeft';
20 import SettingsLayoutRight from '@proton/components/containers/account/SettingsLayoutRight';
21 import SettingsParagraph from '@proton/components/containers/account/SettingsParagraph';
22 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
23 import { useApi, useNotifications } from '@proton/components/hooks';
24 import { useLoading } from '@proton/hooks';
25 import { getBreaches, 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';
37 import freeUserBreachImg from '@proton/styles/assets/img/breach-alert/img-breaches-found.svg';
38 import freeUserNoBreachImg from '@proton/styles/assets/img/breach-alert/img-no-breaches-found-inactive.svg';
39 import clsx from '@proton/utils/clsx';
40 import noop from '@proton/utils/noop';
42 import { useActiveBreakpoint } from '../../hooks';
43 import BreachInformationCard from './BreachInformationCard';
44 import BreachModal from './BreachModal';
45 import BreachesList from './BreachesList';
46 import EmptyBreachListCard from './EmptyBreachListCard';
47 import NoBreachesView from './NoBreachesView';
48 import { BREACH_API_ERROR, getEnableString, getUpsellText, toCamelCase } from './helpers';
49 import type { ListType, SampleBreach } from './models';
50 import { BREACH_STATE } from './models';
51 import { useBreaches } from './useBreaches';
53 const LIST_STATES_MAP: Record<ListType, BREACH_STATE[]> = {
54 open: [BREACH_STATE.UNREAD, BREACH_STATE.READ],
55 resolved: [BREACH_STATE.RESOLVED],
58 const CredentialLeakSection = () => {
59 const handleError = useErrorHandler();
60 const [loading, withLoading] = useLoading();
61 const [breachesLoading] = useLoading();
62 const [toggleLoading, withToggleLoading] = useLoading();
63 const [actionLoading, withActionLoading] = useLoading();
64 const [openSubscriptionModal] = useSubscriptionModal();
66 const [user] = useUser();
67 const [userSettings] = useUserSettings();
68 const { createNotification } = useNotifications();
69 const breachModal = useModalStateObject();
70 const { viewportWidth } = useActiveBreakpoint();
71 const { breaches: breachList, actions } = useBreaches();
73 const [selectedBreachID, setSelectedBreachID] = useState<string | null>(null);
74 const [listType, setListType] = useState<ListType>('open');
75 const [total, setTotal] = useState<number | null>(null);
76 const [error, setError] = useState<{ message: string } | null>(null);
77 const [openModal, setOpenModal] = useState<boolean>(false);
78 const [sample, setSample] = useState<SampleBreach | null>(null);
79 const [hasBeenInteractedWith, setHasBeenInteractedWith] = useState<boolean>(false);
81 // TODO: change nums to constants
82 const [hasAlertsEnabled, setHasAlertsEnabled] = useState<boolean>(
83 userSettings.BreachAlerts.Eligible === 1 && userSettings.BreachAlerts.Value === 1
85 const isPaidUser = user.isPaid;
92 const fetchLeakData = async () => {
94 const { Breaches, Samples, IsEligible, Count } = await api(getBreaches());
97 const breaches = toCamelCase(Breaches);
98 actions.load(breaches);
100 setSelectedBreachID(breaches[0].id);
103 const fetchedSample = toCamelCase(Samples);
104 setSample(fetchedSample[0]);
108 const { message, code } = getApiError(e);
109 if (code === BREACH_API_ERROR.GENERIC) {
110 setError({ message: message });
117 withLoading(fetchLeakData()).catch(noop);
118 }, [hasAlertsEnabled]);
121 const handleBreachModal = () => {
122 if (!loading && viewportWidth['<=medium'] && openModal) {
123 breachModal.openModal(true);
124 setOpenModal(!openModal);
128 }, [loading, openModal, viewportWidth]);
130 // TODO: if BE api returns read and opened breaches, can remove filter
131 const viewingBreachList = breachList.filter((breach) => LIST_STATES_MAP[listType].includes(breach.resolvedState));
132 const viewingBreach = viewingBreachList.find((b) => b.id === selectedBreachID) ?? viewingBreachList[0];
134 const isFirstItemUnread =
135 viewingBreachList.length > 0 && viewingBreachList[0].resolvedState === BREACH_STATE.UNREAD;
136 const firstBreach = viewingBreachList[0];
138 const markAsResolvedBreach = async () => {
139 if (!viewingBreach) {
143 await withActionLoading(
146 ID: viewingBreach.id,
147 State: BREACH_STATE.RESOLVED,
151 actions.resolve(viewingBreach);
152 setSelectedBreachID(null);
158 const markAsOpenBreach = async () => {
159 if (!viewingBreach) {
165 ID: viewingBreach.id,
166 State: BREACH_STATE.READ,
169 actions.open(viewingBreach);
175 const handleEnableBreachAlertToggle = async (newToggleState: boolean) => {
177 const [action, notification] = newToggleState
178 ? [enableBreachAlert, c('Notification').t`Dark Web Monitoring has been enabled`]
179 : [disableBreachAlert, c('Notification').t`Dark Web Monitoring has been disabled`];
181 await withToggleLoading(api(action()));
182 createNotification({ text: notification });
183 setHasAlertsEnabled(newToggleState);
189 // need upsellRef to differentiate between breach alert upsells in account and inbox
190 const upsellRef = getUpsellRef({
191 app: APP_UPSELL_REF_PATH.ACCOUNT_UPSELL_REF_PATH,
192 component: UPSELL_COMPONENT.TOGGLE,
193 feature: MAIL_UPSELL_PATHS.BREACH_ALERTS,
196 const handleUpgrade = () => {
197 openSubscriptionModal({
198 step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
200 mode: 'upsell-modal',
202 onSubscribed: () => {
203 handleEnableBreachAlertToggle(true);
210 if (viewingBreach === firstBreach && isFirstItemUnread && hasBeenInteractedWith) {
213 if (viewingBreach !== firstBreach && viewingBreach.resolvedState === BREACH_STATE.UNREAD) {
217 }, [viewingBreach, hasBeenInteractedWith]);
219 const href = getKnowledgeBaseUrl('/dark-web-monitoring');
220 // 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?>
221 const dataBreachLink = (
222 <Href key={'breach'} className="inline-block" href={href}>{c('Link').t`How does monitoring work?`}</Href>
225 const breachAlertIntroText = (
228 // 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?>
230 .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.`
236 const breachAlertInfoSharing = (
239 .t`${BRAND_NAME} never shares your information with third parties. Data comes from ${BRAND_NAME}'s own analyses and Constella Intelligence.`}{' '}
241 // 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.
243 .t`Support for monitoring of custom domains and non-${BRAND_NAME} email addresses is coming soon.`
248 // translator: full sentence is: Get notified if your password or other personal data was leaked. <Learn more>
249 const learnMoreLinkNoBreach = <Href href={href} className="inline-block">{c('Link').t`Learn more`}</Href>;
251 // 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>
252 const learnMoreLinkBreach = (
253 <Href href={href} className="inline-block color-danger">{c('Link').t`Learn more`}</Href>
258 <SettingsSectionWide>
261 return <GenericError className="text-center">{error.message}</GenericError>;
264 return <Loader size="medium" />;
269 <div className="flex flex-nowrap">
270 <div className="flex-1">
271 {breachAlertIntroText}
275 .jt`Get notified if your password or other personal data was leaked. ${learnMoreLinkNoBreach}`}
280 className="flex flex-nowrap color-danger p-4 rounded"
281 style={{ 'background-color': 'var(--signal-danger-minor-2)' }}
284 name="exclamation-circle-filled"
285 className="shrink-0 mt-0.5 mr-2"
287 <span className="flex-1">
288 {getUpsellText(sample, total, learnMoreLinkBreach, true)}
296 <label className="text-semibold" htmlFor="data-breach-toggle">
297 <span className="mr-2">
298 {getEnableString(DARK_WEB_MONITORING_NAME)}
301 </SettingsLayoutLeft>
302 <SettingsLayoutRight isToggleContainer>
304 id="data-breach-toggle"
307 onClick={handleUpgrade}
309 </SettingsLayoutRight>
313 <div className="hidden lg:flex">
315 src={total === 0 ? freeUserNoBreachImg : freeUserBreachImg}
327 {breachAlertIntroText}
328 {breachAlertInfoSharing}
332 <label className="text-semibold" htmlFor="data-breach-toggle">
333 <span className="mr-2">{getEnableString(DARK_WEB_MONITORING_NAME)}</span>
335 </SettingsLayoutLeft>
336 <SettingsLayoutRight isToggleContainer>
338 id="data-breach-toggle"
340 checked={hasAlertsEnabled}
341 loading={toggleLoading}
342 onChange={({ target }) => {
343 void handleEnableBreachAlertToggle(target.checked);
346 </SettingsLayoutRight>
353 className="flex flex-nowrap lg:flex-row w-full max-h-custom lg:max-h-custom"
354 style={{ '--max-h-custom': '40vh', '--lg-max-h-custom': '90vh' }}
358 selectedID={selectedBreachID}
360 setSelectedBreachID(id);
362 if (!hasBeenInteractedWith) {
363 setHasBeenInteractedWith(true);
366 isPaidUser={isPaidUser}
369 onViewTypeChange={setListType}
371 {viewingBreachList.length === 0 && (
374 'flex relative w-full md:w-2/3',
375 viewportWidth['<=medium'] && 'hidden'
378 <EmptyBreachListCard listType={listType} />
384 'relative w-full md:w-2/3',
385 viewportWidth['<=medium'] && 'hidden'
387 onMouseEnter={() => {
388 if (!hasBeenInteractedWith) {
389 setHasBeenInteractedWith(true);
393 <BreachInformationCard
394 breachData={viewingBreach}
395 onResolve={markAsResolvedBreach}
396 onOpen={() => withActionLoading(markAsOpenBreach())}
397 isMutating={breachesLoading}
398 loading={actionLoading}
407 </SettingsSectionWide>
408 {breachModal.render && (
410 modalProps={breachModal.modalProps}
411 breachData={viewingBreach}
414 actions.resolve(viewingBreach);
423 export default CredentialLeakSection;