Merge branch 'DRVDOC-1260' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / recovery / RecoveryCard.tsx
blob9ddd6339062845538f3523ba6cf69b3dafce1476
1 import { c } from 'ttag';
3 import { useUser } from '@proton/account/user/hooks';
4 import { useUserSettings } from '@proton/account/userSettings/hooks';
5 import { ButtonLike, Href } from '@proton/atoms';
6 import Icon, { type IconName } from '@proton/components/components/icon/Icon';
7 import AppLink from '@proton/components/components/link/AppLink';
8 import Loader from '@proton/components/components/loader/Loader';
9 import SettingsSectionTitle from '@proton/components/containers/account/SettingsSectionTitle';
10 import getBoldFormattedText from '@proton/components/helpers/getBoldFormattedText';
11 import useIsRecoveryFileAvailable from '@proton/components/hooks/recoveryFile/useIsRecoveryFileAvailable';
12 import useIsSecurityCheckupAvailable from '@proton/components/hooks/securityCheckup/useIsSecurityCheckupAvailable';
13 import useSecurityCheckup from '@proton/components/hooks/securityCheckup/useSecurityCheckup';
14 import useHasOutdatedRecoveryFile from '@proton/components/hooks/useHasOutdatedRecoveryFile';
15 import useIsDataRecoveryAvailable from '@proton/components/hooks/useIsDataRecoveryAvailable';
16 import useIsMnemonicAvailable from '@proton/components/hooks/useIsMnemonicAvailable';
17 import useIsSentinelUser from '@proton/components/hooks/useIsSentinelUser';
18 import useRecoverySecrets from '@proton/components/hooks/useRecoverySecrets';
19 import useRecoveryStatus from '@proton/components/hooks/useRecoveryStatus';
20 import { SECURITY_CHECKUP_PATHS } from '@proton/shared/lib/constants';
21 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
22 import { MNEMONIC_STATUS } from '@proton/shared/lib/interfaces';
23 import SecurityCheckupCohort from '@proton/shared/lib/interfaces/securityCheckup/SecurityCheckupCohort';
24 import clsx from '@proton/utils/clsx';
25 import isTruthy from '@proton/utils/isTruthy';
27 import type { RecoveryCardStatusProps } from './RecoveryCardStatus';
28 import RecoveryCardStatus from './RecoveryCardStatus';
29 import getSentinelRecoveryProps from './getSentinelRecoveryProps';
31 interface SentinelUserRecoveryCardProps {
32     ids: {
33         account: string;
34         data: string;
35     };
36     canDisplayNewSentinelSettings?: boolean;
37     isSentinelUser: boolean;
40 const SentinelUserRecoveryCard = ({
41     ids,
42     canDisplayNewSentinelSettings,
43     isSentinelUser,
44 }: SentinelUserRecoveryCardProps) => {
45     const [user] = useUser();
46     const [userSettings, loadingUserSettings] = useUserSettings();
47     const [{ accountRecoveryStatus, dataRecoveryStatus }, loadingRecoveryStatus] = useRecoveryStatus();
49     const [isRecoveryFileAvailable, loadingIsRecoveryFileAvailable] = useIsRecoveryFileAvailable();
50     const [isMnemonicAvailable, loadingIsMnemonicAvailable] = useIsMnemonicAvailable();
51     const [isDataRecoveryAvailable, loadingIsDataRecoveryAvailable] = useIsDataRecoveryAvailable();
53     const hasOutdatedRecoveryFile = useHasOutdatedRecoveryFile();
54     const recoverySecrets = useRecoverySecrets();
56     if (
57         loadingRecoveryStatus ||
58         loadingIsDataRecoveryAvailable ||
59         loadingIsRecoveryFileAvailable ||
60         loadingIsMnemonicAvailable ||
61         loadingUserSettings
62     ) {
63         return <Loader />;
64     }
66     const hasMnemonic = isMnemonicAvailable && user.MnemonicStatus === MNEMONIC_STATUS.SET;
68     const boldImperative = (
69         <b key="imperative-bold-text">{
70             // translator: Full sentence is 'If you lose your login details and need to reset your account, it’s imperative that you have both an account recovery and data recovery method in place, otherwise you might not be able to access any of your emails, contacts, or files.'
71             c('Info').t`it’s imperative`
72         }</b>
73     );
75     const boldAccountAndRecovery = (
76         <b key="account-and-recovery-bold-text">{
77             // translator: Full sentence is 'If you lose your login details and need to reset your account, it’s imperative that you have both an account recovery and data recovery method in place, otherwise you might not be able to access any of your emails, contacts, or files.'
78             c('Info').t`account recovery and data recovery method`
79         }</b>
80     );
82     const boldAccountRecovery = (
83         <b key="account-recovery-bold-text">{
84             // translator: Full sentence is 'If you lose your login details and need to reset your account, it’s imperative that you have an account recovery method in place.'
85             c('Info').t`account recovery method`
86         }</b>
87     );
89     const sentinelAccountProps: RecoveryCardStatusProps = (() => {
90         if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED) {
91             return {
92                 type: 'danger',
93                 statusText: c('Info').t`Outdated recovery phrase; update to ensure access to your data`,
94                 callToActions: [
95                     {
96                         text: c('Info').t`Update recovery phrase`,
97                         path: `/recovery#${ids.data}`,
98                     },
99                 ],
100             };
101         }
103         return getSentinelRecoveryProps(userSettings.Email, userSettings.Phone, hasMnemonic, ids);
104     })();
106     const accountStatusProps: RecoveryCardStatusProps | undefined = (() => {
107         if (accountRecoveryStatus === 'complete') {
108             return {
109                 type: 'success',
110                 statusText: c('Info').t`Your account recovery method is set`,
111                 callToActions: [],
112             };
113         }
115         const emailCTA = {
116             text:
117                 !!userSettings.Email.Value && !userSettings.Email.Reset
118                     ? c('Info').t`Allow recovery by email`
119                     : c('Info').t`Add a recovery email address`,
120             path: `/recovery#${ids.account}`,
121         };
123         const phoneCTA = {
124             text:
125                 !!userSettings.Phone.Value && !userSettings.Phone.Reset
126                     ? c('Info').t`Allow recovery by phone`
127                     : c('Info').t`Add a recovery phone number`,
128             path: `/recovery#${ids.account}`,
129         };
131         if (user.MnemonicStatus === MNEMONIC_STATUS.SET) {
132             return {
133                 type: 'info',
134                 statusText: c('Info').t`To ensure continuous access to your account, set an account recovery method`,
135                 callToActions: [emailCTA, phoneCTA],
136             };
137         }
139         return {
140             type: 'warning',
141             statusText: c('Info').t`No account recovery method set; you are at risk of losing access to your account`,
142             callToActions: [emailCTA, phoneCTA],
143         };
144     })();
146     const dataStatusProps: RecoveryCardStatusProps | undefined = (() => {
147         if (!isRecoveryFileAvailable && !isMnemonicAvailable) {
148             return;
149         }
151         const recoveryFileCTA = isRecoveryFileAvailable && {
152             text: c('Info').t`Download recovery file`,
153             path: `/recovery#${ids.data}`,
154         };
156         const updateRecoveryFileCTA = isRecoveryFileAvailable && {
157             text: c('Info').t`Update recovery file`,
158             path: `/recovery#${ids.data}`,
159         };
161         const recoveryPhraseCTA = isMnemonicAvailable && {
162             text: c('Info').t`Set recovery phrase`,
163             path: `/recovery#${ids.data}`,
164         };
166         const updateRecoveryPhraseCTA = isMnemonicAvailable && {
167             text: c('Info').t`Update recovery phrase`,
168             path: `/recovery#${ids.data}`,
169         };
171         if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED && hasOutdatedRecoveryFile) {
172             return {
173                 type: 'danger',
174                 statusText: c('Info').t`Outdated recovery methods; update to ensure access to your data`,
175                 callToActions: [updateRecoveryPhraseCTA, updateRecoveryFileCTA].filter(isTruthy),
176             };
177         }
179         if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED) {
180             return {
181                 type: 'danger',
182                 statusText: c('Info').t`Outdated recovery phrase; update to ensure access to your data`,
183                 callToActions: [updateRecoveryPhraseCTA, recoverySecrets.length === 0 && recoveryFileCTA].filter(
184                     isTruthy
185                 ),
186             };
187         }
189         if (hasOutdatedRecoveryFile) {
190             return {
191                 type: 'danger',
192                 statusText: c('Info').t`Outdated recovery file; update to ensure access to your data`,
193                 callToActions: [
194                     user.MnemonicStatus !== MNEMONIC_STATUS.SET && recoveryPhraseCTA,
195                     updateRecoveryFileCTA,
196                 ].filter(isTruthy),
197             };
198         }
200         if (dataRecoveryStatus === 'complete') {
201             return {
202                 type: 'success',
203                 statusText: c('Info').t`Your data recovery method is set`,
204                 callToActions: [],
205             };
206         }
208         return {
209             type: 'warning',
210             statusText: c('Info').t`No data recovery method set; you are at risk of losing access to your data`,
211             callToActions: [recoveryPhraseCTA, recoveryFileCTA].filter(isTruthy),
212         };
213     })();
215     return (
216         <div className="rounded border p-8 max-w-custom" style={{ '--max-w-custom': '46em' }}>
217             <SettingsSectionTitle className="h3">
218                 {c('Title').t`Take precautions to avoid data loss!`}
219             </SettingsSectionTitle>
220             <p>
221                 {isDataRecoveryAvailable
222                     ? // translator: Full sentence is 'If you lose your login details and need to reset your account, it’s imperative that you have both an account recovery and data recovery method in place, otherwise you might not be able to access any of your emails, contacts, or files.'
223                       c('Info')
224                           .jt`If you lose your password and need to recover your account, ${boldImperative} that you have both an ${boldAccountAndRecovery} in place, otherwise you might not be able to access any of your emails, contacts, or files.`
225                     : // translator: Full sentence is 'If you lose your login details and need to reset your account, it’s imperative that you have an account recovery method in place.'
226                       c('Info')
227                           .jt`If you lose your password and need to recover your account, ${boldImperative} that you have an ${boldAccountRecovery} in place.`}
228                 <br />
229                 <Href href={getKnowledgeBaseUrl('/set-account-recovery-methods')}>
230                     {c('Link').t`Why set recovery methods?`}
231                 </Href>
232             </p>
234             <h3 className="text-bold text-rg mb-4">{c('Title').t`Your recovery status`}</h3>
236             <ul className="unstyled m-0">
237                 {canDisplayNewSentinelSettings && isSentinelUser ? (
238                     <li>
239                         <RecoveryCardStatus {...sentinelAccountProps} />
240                     </li>
241                 ) : (
242                     <>
243                         {accountStatusProps && (
244                             <li>
245                                 <RecoveryCardStatus {...accountStatusProps} />
246                             </li>
247                         )}
248                         {dataStatusProps && (
249                             <li className="mt-2">
250                                 <RecoveryCardStatus {...dataStatusProps} />
251                             </li>
252                         )}
253                     </>
254                 )}
255             </ul>
256         </div>
257     );
260 const GenericSecurityCheckupCard = ({
261     title,
262     subtitle,
263     icon,
264     color,
265     description,
266     cta,
267 }: {
268     title: string;
269     subtitle: string;
270     icon: IconName;
271     color: 'success' | 'danger' | 'info' | 'warning';
272     description?: string | ReturnType<typeof getBoldFormattedText>;
273     cta: string;
274 }) => {
275     const securityCheckupParams = new URLSearchParams({
276         back: encodeURIComponent(window.location.href),
277         source: 'recovery_settings',
278     });
280     return (
281         <div className="rounded border max-w-custom p-8 flex flex-column gap-8" style={{ '--max-w-custom': '46em' }}>
282             <div className="flex flex-nowrap items-center gap-4">
283                 <div className={clsx('rounded p-2 overflow-hidden', `security-checkup-color--${color}`)}>
284                     <Icon name={icon} size={10} />
285                 </div>
286                 <div>
287                     <h2 className="h3 text-bold mb-0">{title}</h2>
288                     <div className="color-weak max-w-custom">{subtitle}</div>
289                 </div>
290             </div>
292             <div>{description}</div>
294             <ButtonLike
295                 className="self-start"
296                 as={AppLink}
297                 to={`${SECURITY_CHECKUP_PATHS.ROOT}?${securityCheckupParams.toString()}`}
298                 color="norm"
299             >
300                 {cta}
301             </ButtonLike>
302         </div>
303     );
306 const SecurityCheckupCard = () => {
307     const securityCheckup = useSecurityCheckup();
309     const { actions, furtherActions, cohort } = securityCheckup;
311     if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_MULTIPLE) {
312         return (
313             <GenericSecurityCheckupCard
314                 title={c('Safety review').t`Your account and data can be recovered`}
315                 subtitle={c('Safety review').t`Your account is fully secure.`}
316                 icon="pass-shield-ok"
317                 color="success"
318                 description={c('Safety review')
319                     .t`Your account and data can be recovered. Check if you can still access your recovery methods.`}
320                 cta={c('Safety review').t`Check account security`}
321             />
322         );
323     }
325     if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_SINGLE) {
326         return (
327             <GenericSecurityCheckupCard
328                 title={c('Safety review').t`Safeguard your account`}
329                 subtitle={c('Safety review').t`You have recommended actions.`}
330                 icon="pass-shield-warning"
331                 color="info"
332                 description={c('Safety review')
333                     .t`Your account and data can be recovered. You have recommended actions to safeguard your account further.`}
334                 cta={c('Safety review').t`Safeguard account now`}
335             />
336         );
337     }
339     if (cohort === SecurityCheckupCohort.ACCOUNT_RECOVERY_ENABLED) {
340         return (
341             <GenericSecurityCheckupCard
342                 title={c('Safety review').t`Safeguard your account`}
343                 subtitle={c('Safety review').t`You are at risk of losing access to your data.`}
344                 icon="pass-shield-warning"
345                 color="warning"
346                 description={getBoldFormattedText(
347                     c('Safety review')
348                         .t`If you lose your login details and need to reset your account, **it’s imperative** that you have both an **account recovery and data recovery method** in place, otherwise you might not be able to access any of your emails, contacts, files or passwords.`
349                 )}
350                 cta={c('Safety review').t`Safeguard account now`}
351             />
352         );
353     }
355     if (cohort === SecurityCheckupCohort.NO_RECOVERY_METHOD) {
356         return (
357             <GenericSecurityCheckupCard
358                 title={c('Safety review').t`Safeguard your account`}
359                 subtitle={c('Safety review').t`You are at risk of losing access to your account and data.`}
360                 icon="pass-shield-warning"
361                 color="danger"
362                 description={getBoldFormattedText(
363                     c('Safety review')
364                         .t`If you lose your login details and need to reset your account, **it’s imperative** that you have both an **account recovery and data recovery method** in place, otherwise you might not be able to access any of your emails, contacts, files or passwords.`
365                 )}
366                 cta={c('Safety review').t`Safeguard account now`}
367             />
368         );
369     }
371     if (actions.length || furtherActions.length) {
372         return (
373             <GenericSecurityCheckupCard
374                 title={c('Safety review').t`Safeguard your account`}
375                 subtitle={c('Safety review').t`You have recommended actions.`}
376                 icon="pass-shield-warning"
377                 color="info"
378                 cta={c('Safety review').t`Safeguard account now`}
379             />
380         );
381     }
383     return (
384         <GenericSecurityCheckupCard
385             title={c('Safety review').t`Safeguard your account`}
386             subtitle={c('Safety review').t`Your account is fully secure.`}
387             icon="pass-shield-ok"
388             color="success"
389             cta={c('Safety review').t`Check account security`}
390         />
391     );
394 interface RecoveryCardProps {
395     ids: {
396         account: string;
397         data: string;
398     };
399     canDisplayNewSentinelSettings?: boolean;
402 const RecoveryCard = ({ ids, canDisplayNewSentinelSettings }: RecoveryCardProps) => {
403     const [{ isSentinelUser }, loadingIsSentinelUser] = useIsSentinelUser();
404     const isSecurityCheckupAvailable = useIsSecurityCheckupAvailable();
406     if (loadingIsSentinelUser) {
407         return <Loader />;
408     }
410     if (isSentinelUser || !isSecurityCheckupAvailable) {
411         return (
412             <SentinelUserRecoveryCard
413                 ids={ids}
414                 canDisplayNewSentinelSettings={canDisplayNewSentinelSettings}
415                 isSentinelUser={isSentinelUser}
416             />
417         );
418     }
420     return <SecurityCheckupCard />;
423 export default RecoveryCard;