Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / credentialLeak / CredentialLeakSection.tsx
blobe02f5273924b1fb30623832f5cd79ee6c1d33e05
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';
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 {
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();
74     const api = useApi();
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
94     );
95     const [hasEmailsEnabled, setHasEmailsEnabled] = useState<boolean>(
96         BreachAlerts.EmailNotifications === DARK_WEB_MONITORING_EMAILS_STATE.ENABLED
97     );
98     const isPaidUser = user.isPaid;
100     const metrics = {
101         source: 'upsells',
102     } as const;
104     useEffect(() => {
105         const fetchLeakData = async () => {
106             try {
107                 const { Breaches, Samples, IsEligible, Count } = await api(getBreaches());
109                 if (IsEligible) {
110                     const breaches = toCamelCase(Breaches);
111                     actions.load(breaches);
112                     if (Count > 0) {
113                         setSelectedBreachID(breaches[0].id);
114                     }
115                 } else {
116                     const fetchedSample = toCamelCase(Samples);
117                     setSample(fetchedSample[0]);
118                 }
119                 setTotal(Count);
120             } catch (e) {
121                 const { message, code } = getApiError(e);
122                 if (code === BREACH_API_ERROR.GENERIC) {
123                     setError({ message: message });
124                     return;
125                 } else {
126                     handleError(e);
127                 }
128             }
129         };
131         withLoading(fetchLeakData()).catch(noop);
132     }, [hasAlertsEnabled]);
134     useEffect(() => {
135         const handleBreachModal = () => {
136             if (!loading && viewportWidth['<=medium'] && openModal) {
137                 breachModal.openModal(true);
138                 setOpenModal(!openModal);
139             }
140         };
141         handleBreachModal();
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) {
154             return;
155         }
156         try {
157             await withActionLoading(
158                 api(
159                     updateBreachState({
160                         ID: viewingBreach.id,
161                         State: BREACH_STATE.RESOLVED,
162                     })
163                 )
164             );
165             actions.resolve(viewingBreach);
166             setSelectedBreachID(null);
167         } catch (e) {
168             handleError(e);
169         }
170     };
172     const markAsOpenBreach = async () => {
173         if (!viewingBreach) {
174             return;
175         }
176         try {
177             await api(
178                 updateBreachState({
179                     ID: viewingBreach.id,
180                     State: BREACH_STATE.READ,
181                 })
182             );
183             actions.open(viewingBreach);
184         } catch (e) {
185             handleError(e);
186         }
187     };
189     const handleEnableBreachAlertToggle = async (newToggleState: boolean) => {
190         if (newToggleState === hasAlertsEnabled) {
191             return;
192         }
193         try {
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);
201         } catch (e) {
202             handleError(e);
203         }
204     };
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`;
210         try {
211             await withEmailToggleLoading(api(updateBreachEmailNotificationsState({ Enabled: newState })));
212             createNotification({ text: notification });
213             setHasEmailsEnabled(newState);
214         } catch (e) {
215             handleError(e);
216         }
217     };
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,
224     });
226     const handleUpgrade = () => {
227         openSubscriptionModal({
228             step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
229             metrics,
230             mode: 'upsell-modal',
231             upsellRef,
232             onSubscribed: () => {
233                 handleEnableBreachAlertToggle(true);
234                 return;
235             },
236         });
237     };
239     useEffect(() => {
240         if (viewingBreach === firstBreach && isFirstItemUnread && hasBeenInteractedWith) {
241             markAsOpenBreach();
242         } else {
243             if (viewingBreach !== firstBreach && viewingBreach.resolvedState === BREACH_STATE.UNREAD) {
244                 markAsOpenBreach();
245             }
246         }
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>
253     );
255     const breachAlertIntroText = (
256         <SettingsParagraph>
257             {
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?>
259                 c('Info')
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.`
261             }{' '}
262             {dataBreachLink}
263         </SettingsParagraph>
264     );
266     const breachAlertInfoSharing = (
267         <SettingsParagraph>
268             {c('Info')
269                 .t`${BRAND_NAME} never shares your information with third parties. Data comes from ${BRAND_NAME}'s own analyses and Constella Intelligence.`}{' '}
270             {
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.
272                 c('Info')
273                     .t`Support for monitoring of custom domains and non-${BRAND_NAME} email addresses is coming soon.`
274             }
275         </SettingsParagraph>
276     );
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>
284     );
286     return (
287         <>
288             <SettingsSectionWide>
289                 {(() => {
290                     if (error) {
291                         return <GenericError className="text-center">{error.message}</GenericError>;
292                     }
293                     if (loading) {
294                         return <Loader size="medium" />;
295                     }
297                     if (!isPaidUser) {
298                         return (
299                             <div className="flex flex-nowrap">
300                                 <div className="flex-1">
301                                     {breachAlertIntroText}
302                                     {total === 0 ? (
303                                         <SettingsParagraph>
304                                             {c('Info')
305                                                 .jt`Get notified if your password or other personal data was leaked. ${learnMoreLinkNoBreach}`}
306                                         </SettingsParagraph>
307                                     ) : (
308                                         <SettingsParagraph>
309                                             <div
310                                                 className="flex flex-nowrap color-danger p-4 rounded"
311                                                 style={{ 'background-color': 'var(--signal-danger-minor-2)' }}
312                                             >
313                                                 <Icon
314                                                     name="exclamation-circle-filled"
315                                                     className="shrink-0 mt-0.5 mr-2"
316                                                 />
317                                                 <span className="flex-1">
318                                                     {getUpsellText(sample, total, learnMoreLinkBreach, true)}
319                                                 </span>
320                                             </div>
321                                         </SettingsParagraph>
322                                     )}
324                                     <SettingsLayout>
325                                         <SettingsLayoutLeft>
326                                             <label className="text-semibold" htmlFor="data-breach-toggle">
327                                                 <span className="mr-2">
328                                                     {getEnableString(DARK_WEB_MONITORING_NAME)}
329                                                 </span>
330                                             </label>
331                                         </SettingsLayoutLeft>
332                                         <SettingsLayoutRight isToggleContainer>
333                                             <Toggle
334                                                 id="data-breach-toggle"
335                                                 disabled={false}
336                                                 checked={false}
337                                                 onClick={handleUpgrade}
338                                             />
339                                         </SettingsLayoutRight>
340                                     </SettingsLayout>
341                                 </div>
343                                 <div className="hidden lg:flex">
344                                     <img
345                                         src={total === 0 ? freeUserNoBreachImg : freeUserBreachImg}
346                                         alt=""
347                                         width={300}
348                                         className="m-auto"
349                                     />
350                                 </div>
351                             </div>
352                         );
353                     }
355                     return (
356                         <>
357                             {breachAlertIntroText}
358                             {breachAlertInfoSharing}
359                             <BreachMonitoringToggle
360                                 enabled={hasAlertsEnabled}
361                                 loading={toggleLoading}
362                                 onToggle={handleEnableBreachAlertToggle}
363                             />
364                             {canDisplayDWMEmailToggle && (
365                                 <BreachEmailToggle
366                                     enabled={hasEmailsEnabled}
367                                     loading={emailToggleLoading}
368                                     onToggle={handleEmailNotificationsToggle}
369                                 />
370                             )}
371                             {hasAlertsEnabled &&
372                                 (total === 0 ? (
373                                     <NoBreachesView />
374                                 ) : (
375                                     <div
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' }}
378                                     >
379                                         <BreachesList
380                                             data={breachList}
381                                             selectedID={selectedBreachID}
382                                             onSelect={(id) => {
383                                                 setSelectedBreachID(id);
384                                                 setOpenModal(true);
385                                                 if (!hasBeenInteractedWith) {
386                                                     setHasBeenInteractedWith(true);
387                                                 }
388                                             }}
389                                             isPaidUser={isPaidUser}
390                                             total={total}
391                                             type={listType}
392                                             onViewTypeChange={setListType}
393                                         />
394                                         {viewingBreachList.length === 0 && (
395                                             <div
396                                                 className={clsx(
397                                                     'flex relative w-full md:w-2/3',
398                                                     viewportWidth['<=medium'] && 'hidden'
399                                                 )}
400                                             >
401                                                 <EmptyBreachListCard listType={listType} />
402                                             </div>
403                                         )}
404                                         {viewingBreach && (
405                                             <div
406                                                 className={clsx(
407                                                     'relative w-full md:w-2/3',
408                                                     viewportWidth['<=medium'] && 'hidden'
409                                                 )}
410                                                 onMouseEnter={() => {
411                                                     if (!hasBeenInteractedWith) {
412                                                         setHasBeenInteractedWith(true);
413                                                     }
414                                                 }}
415                                             >
416                                                 <BreachInformationCard
417                                                     breachData={viewingBreach}
418                                                     onResolve={markAsResolvedBreach}
419                                                     onOpen={() => withActionLoading(markAsOpenBreach())}
420                                                     isMutating={breachesLoading}
421                                                     loading={actionLoading}
422                                                 />
423                                             </div>
424                                         )}
425                                     </div>
426                                 ))}
427                         </>
428                     );
429                 })()}
430             </SettingsSectionWide>
431             {breachModal.render && (
432                 <BreachModal
433                     modalProps={breachModal.modalProps}
434                     breachData={viewingBreach}
435                     onResolve={() => {
436                         if (viewingBreach) {
437                             actions.resolve(viewingBreach);
438                         }
439                     }}
440                 />
441             )}
442         </>
443     );
446 export default CredentialLeakSection;