start removing account
[ProtonMail-WebClient.git] / packages / components / containers / credentialLeak / CredentialLeakSection.tsx
blob5d39f01647468d7dc2b224df841ae0032835d0a4
1 import { useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { Href } from '@proton/atoms';
6 import {
7     GenericError,
8     SUBSCRIPTION_STEPS,
9     useErrorHandler,
10     useSubscriptionModal,
11     useUser,
12     useUserSettings,
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';
28 import {
29     APP_UPSELL_REF_PATH,
30     BRAND_NAME,
31     DARK_WEB_MONITORING_NAME,
32     MAIL_UPSELL_PATHS,
33     UPSELL_COMPONENT,
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();
65     const api = useApi();
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
84     );
85     const isPaidUser = user.isPaid;
87     const metrics = {
88         source: 'upsells',
89     } as const;
91     useEffect(() => {
92         const fetchLeakData = async () => {
93             try {
94                 const { Breaches, Samples, IsEligible, Count } = await api(getBreaches());
96                 if (IsEligible) {
97                     const breaches = toCamelCase(Breaches);
98                     actions.load(breaches);
99                     if (Count > 0) {
100                         setSelectedBreachID(breaches[0].id);
101                     }
102                 } else {
103                     const fetchedSample = toCamelCase(Samples);
104                     setSample(fetchedSample[0]);
105                 }
106                 setTotal(Count);
107             } catch (e) {
108                 const { message, code } = getApiError(e);
109                 if (code === BREACH_API_ERROR.GENERIC) {
110                     setError({ message: message });
111                     return;
112                 } else {
113                     handleError(e);
114                 }
115             }
116         };
117         withLoading(fetchLeakData()).catch(noop);
118     }, [hasAlertsEnabled]);
120     useEffect(() => {
121         const handleBreachModal = () => {
122             if (!loading && viewportWidth['<=medium'] && openModal) {
123                 breachModal.openModal(true);
124                 setOpenModal(!openModal);
125             }
126         };
127         handleBreachModal();
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) {
140             return;
141         }
142         try {
143             await withActionLoading(
144                 api(
145                     updateBreachState({
146                         ID: viewingBreach.id,
147                         State: BREACH_STATE.RESOLVED,
148                     })
149                 )
150             );
151             actions.resolve(viewingBreach);
152             setSelectedBreachID(null);
153         } catch (e) {
154             handleError(e);
155         }
156     };
158     const markAsOpenBreach = async () => {
159         if (!viewingBreach) {
160             return;
161         }
162         try {
163             await api(
164                 updateBreachState({
165                     ID: viewingBreach.id,
166                     State: BREACH_STATE.READ,
167                 })
168             );
169             actions.open(viewingBreach);
170         } catch (e) {
171             handleError(e);
172         }
173     };
175     const handleEnableBreachAlertToggle = async (newToggleState: boolean) => {
176         try {
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);
184         } catch (e) {
185             handleError(e);
186         }
187     };
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,
194     });
196     const handleUpgrade = () => {
197         openSubscriptionModal({
198             step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
199             metrics,
200             mode: 'upsell-modal',
201             upsellRef,
202             onSubscribed: () => {
203                 handleEnableBreachAlertToggle(true);
204                 return;
205             },
206         });
207     };
209     useEffect(() => {
210         if (viewingBreach === firstBreach && isFirstItemUnread && hasBeenInteractedWith) {
211             markAsOpenBreach();
212         } else {
213             if (viewingBreach !== firstBreach && viewingBreach.resolvedState === BREACH_STATE.UNREAD) {
214                 markAsOpenBreach();
215             }
216         }
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>
223     );
225     const breachAlertIntroText = (
226         <SettingsParagraph>
227             {
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?>
229                 c('Info')
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.`
231             }{' '}
232             {dataBreachLink}
233         </SettingsParagraph>
234     );
236     const breachAlertInfoSharing = (
237         <SettingsParagraph>
238             {c('Info')
239                 .t`${BRAND_NAME} never shares your information with third parties. Data comes from ${BRAND_NAME}'s own analyses and Constella Intelligence.`}{' '}
240             {
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.
242                 c('Info')
243                     .t`Support for monitoring of custom domains and non-${BRAND_NAME} email addresses is coming soon.`
244             }
245         </SettingsParagraph>
246     );
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>
254     );
256     return (
257         <>
258             <SettingsSectionWide>
259                 {(() => {
260                     if (error) {
261                         return <GenericError className="text-center">{error.message}</GenericError>;
262                     }
263                     if (loading) {
264                         return <Loader size="medium" />;
265                     }
267                     if (!isPaidUser) {
268                         return (
269                             <div className="flex flex-nowrap">
270                                 <div className="flex-1">
271                                     {breachAlertIntroText}
272                                     {total === 0 ? (
273                                         <SettingsParagraph>
274                                             {c('Info')
275                                                 .jt`Get notified if your password or other personal data was leaked. ${learnMoreLinkNoBreach}`}
276                                         </SettingsParagraph>
277                                     ) : (
278                                         <SettingsParagraph>
279                                             <div
280                                                 className="flex flex-nowrap color-danger p-4 rounded"
281                                                 style={{ 'background-color': 'var(--signal-danger-minor-2)' }}
282                                             >
283                                                 <Icon
284                                                     name="exclamation-circle-filled"
285                                                     className="shrink-0 mt-0.5 mr-2"
286                                                 />
287                                                 <span className="flex-1">
288                                                     {getUpsellText(sample, total, learnMoreLinkBreach, true)}
289                                                 </span>
290                                             </div>
291                                         </SettingsParagraph>
292                                     )}
294                                     <SettingsLayout>
295                                         <SettingsLayoutLeft>
296                                             <label className="text-semibold" htmlFor="data-breach-toggle">
297                                                 <span className="mr-2">
298                                                     {getEnableString(DARK_WEB_MONITORING_NAME)}
299                                                 </span>
300                                             </label>
301                                         </SettingsLayoutLeft>
302                                         <SettingsLayoutRight isToggleContainer>
303                                             <Toggle
304                                                 id="data-breach-toggle"
305                                                 disabled={false}
306                                                 checked={false}
307                                                 onClick={handleUpgrade}
308                                             />
309                                         </SettingsLayoutRight>
310                                     </SettingsLayout>
311                                 </div>
313                                 <div className="hidden lg:flex">
314                                     <img
315                                         src={total === 0 ? freeUserNoBreachImg : freeUserBreachImg}
316                                         alt=""
317                                         width={300}
318                                         className="m-auto"
319                                     />
320                                 </div>
321                             </div>
322                         );
323                     }
325                     return (
326                         <>
327                             {breachAlertIntroText}
328                             {breachAlertInfoSharing}
330                             <SettingsLayout>
331                                 <SettingsLayoutLeft>
332                                     <label className="text-semibold" htmlFor="data-breach-toggle">
333                                         <span className="mr-2">{getEnableString(DARK_WEB_MONITORING_NAME)}</span>
334                                     </label>
335                                 </SettingsLayoutLeft>
336                                 <SettingsLayoutRight isToggleContainer>
337                                     <Toggle
338                                         id="data-breach-toggle"
339                                         disabled={false}
340                                         checked={hasAlertsEnabled}
341                                         loading={toggleLoading}
342                                         onChange={({ target }) => {
343                                             void handleEnableBreachAlertToggle(target.checked);
344                                         }}
345                                     />
346                                 </SettingsLayoutRight>
347                             </SettingsLayout>
348                             {hasAlertsEnabled &&
349                                 (total === 0 ? (
350                                     <NoBreachesView />
351                                 ) : (
352                                     <div
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' }}
355                                     >
356                                         <BreachesList
357                                             data={breachList}
358                                             selectedID={selectedBreachID}
359                                             onSelect={(id) => {
360                                                 setSelectedBreachID(id);
361                                                 setOpenModal(true);
362                                                 if (!hasBeenInteractedWith) {
363                                                     setHasBeenInteractedWith(true);
364                                                 }
365                                             }}
366                                             isPaidUser={isPaidUser}
367                                             total={total}
368                                             type={listType}
369                                             onViewTypeChange={setListType}
370                                         />
371                                         {viewingBreachList.length === 0 && (
372                                             <div
373                                                 className={clsx(
374                                                     'flex relative w-full md:w-2/3',
375                                                     viewportWidth['<=medium'] && 'hidden'
376                                                 )}
377                                             >
378                                                 <EmptyBreachListCard listType={listType} />
379                                             </div>
380                                         )}
381                                         {viewingBreach && (
382                                             <div
383                                                 className={clsx(
384                                                     'relative w-full md:w-2/3',
385                                                     viewportWidth['<=medium'] && 'hidden'
386                                                 )}
387                                                 onMouseEnter={() => {
388                                                     if (!hasBeenInteractedWith) {
389                                                         setHasBeenInteractedWith(true);
390                                                     }
391                                                 }}
392                                             >
393                                                 <BreachInformationCard
394                                                     breachData={viewingBreach}
395                                                     onResolve={markAsResolvedBreach}
396                                                     onOpen={() => withActionLoading(markAsOpenBreach())}
397                                                     isMutating={breachesLoading}
398                                                     loading={actionLoading}
399                                                 />
400                                             </div>
401                                         )}
402                                     </div>
403                                 ))}
404                         </>
405                     );
406                 })()}
407             </SettingsSectionWide>
408             {breachModal.render && (
409                 <BreachModal
410                     modalProps={breachModal.modalProps}
411                     breachData={viewingBreach}
412                     onResolve={() => {
413                         if (viewingBreach) {
414                             actions.resolve(viewingBreach);
415                         }
416                     }}
417                 />
418             )}
419         </>
420     );
423 export default CredentialLeakSection;