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 {
36 canDisplayNewSentinelSettings?: boolean;
37 isSentinelUser: boolean;
40 const SentinelUserRecoveryCard = ({
42 canDisplayNewSentinelSettings,
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();
57 loadingRecoveryStatus ||
58 loadingIsDataRecoveryAvailable ||
59 loadingIsRecoveryFileAvailable ||
60 loadingIsMnemonicAvailable ||
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`
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`
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`
89 const sentinelAccountProps: RecoveryCardStatusProps = (() => {
90 if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED) {
93 statusText: c('Info').t`Outdated recovery phrase; update to ensure access to your data`,
96 text: c('Info').t`Update recovery phrase`,
97 path: `/recovery#${ids.data}`,
103 return getSentinelRecoveryProps(userSettings.Email, userSettings.Phone, hasMnemonic, ids);
106 const accountStatusProps: RecoveryCardStatusProps | undefined = (() => {
107 if (accountRecoveryStatus === 'complete') {
110 statusText: c('Info').t`Your account recovery method is set`,
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}`,
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}`,
131 if (user.MnemonicStatus === MNEMONIC_STATUS.SET) {
134 statusText: c('Info').t`To ensure continuous access to your account, set an account recovery method`,
135 callToActions: [emailCTA, phoneCTA],
141 statusText: c('Info').t`No account recovery method set; you are at risk of losing access to your account`,
142 callToActions: [emailCTA, phoneCTA],
146 const dataStatusProps: RecoveryCardStatusProps | undefined = (() => {
147 if (!isRecoveryFileAvailable && !isMnemonicAvailable) {
151 const recoveryFileCTA = isRecoveryFileAvailable && {
152 text: c('Info').t`Download recovery file`,
153 path: `/recovery#${ids.data}`,
156 const updateRecoveryFileCTA = isRecoveryFileAvailable && {
157 text: c('Info').t`Update recovery file`,
158 path: `/recovery#${ids.data}`,
161 const recoveryPhraseCTA = isMnemonicAvailable && {
162 text: c('Info').t`Set recovery phrase`,
163 path: `/recovery#${ids.data}`,
166 const updateRecoveryPhraseCTA = isMnemonicAvailable && {
167 text: c('Info').t`Update recovery phrase`,
168 path: `/recovery#${ids.data}`,
171 if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED && hasOutdatedRecoveryFile) {
174 statusText: c('Info').t`Outdated recovery methods; update to ensure access to your data`,
175 callToActions: [updateRecoveryPhraseCTA, updateRecoveryFileCTA].filter(isTruthy),
179 if (user.MnemonicStatus === MNEMONIC_STATUS.OUTDATED) {
182 statusText: c('Info').t`Outdated recovery phrase; update to ensure access to your data`,
183 callToActions: [updateRecoveryPhraseCTA, recoverySecrets.length === 0 && recoveryFileCTA].filter(
189 if (hasOutdatedRecoveryFile) {
192 statusText: c('Info').t`Outdated recovery file; update to ensure access to your data`,
194 user.MnemonicStatus !== MNEMONIC_STATUS.SET && recoveryPhraseCTA,
195 updateRecoveryFileCTA,
200 if (dataRecoveryStatus === 'complete') {
203 statusText: c('Info').t`Your data recovery method is set`,
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),
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>
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.'
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.'
227 .jt`If you lose your password and need to recover your account, ${boldImperative} that you have an ${boldAccountRecovery} in place.`}
229 <Href href={getKnowledgeBaseUrl('/set-account-recovery-methods')}>
230 {c('Link').t`Why set recovery methods?`}
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 ? (
239 <RecoveryCardStatus {...sentinelAccountProps} />
243 {accountStatusProps && (
245 <RecoveryCardStatus {...accountStatusProps} />
248 {dataStatusProps && (
249 <li className="mt-2">
250 <RecoveryCardStatus {...dataStatusProps} />
260 const GenericSecurityCheckupCard = ({
271 color: 'success' | 'danger' | 'info' | 'warning';
272 description?: string | ReturnType<typeof getBoldFormattedText>;
275 const securityCheckupParams = new URLSearchParams({
276 back: encodeURIComponent(window.location.href),
277 source: 'recovery_settings',
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} />
287 <h2 className="h3 text-bold mb-0">{title}</h2>
288 <div className="color-weak max-w-custom">{subtitle}</div>
292 <div>{description}</div>
295 className="self-start"
297 to={`${SECURITY_CHECKUP_PATHS.ROOT}?${securityCheckupParams.toString()}`}
306 const SecurityCheckupCard = () => {
307 const securityCheckup = useSecurityCheckup();
309 const { actions, furtherActions, cohort } = securityCheckup;
311 if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_MULTIPLE) {
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"
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`}
325 if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_SINGLE) {
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"
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`}
339 if (cohort === SecurityCheckupCohort.ACCOUNT_RECOVERY_ENABLED) {
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"
346 description={getBoldFormattedText(
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.`
350 cta={c('Safety review').t`Safeguard account now`}
355 if (cohort === SecurityCheckupCohort.NO_RECOVERY_METHOD) {
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"
362 description={getBoldFormattedText(
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.`
366 cta={c('Safety review').t`Safeguard account now`}
371 if (actions.length || furtherActions.length) {
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"
378 cta={c('Safety review').t`Safeguard account now`}
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"
389 cta={c('Safety review').t`Check account security`}
394 interface RecoveryCardProps {
399 canDisplayNewSentinelSettings?: boolean;
402 const RecoveryCard = ({ ids, canDisplayNewSentinelSettings }: RecoveryCardProps) => {
403 const [{ isSentinelUser }, loadingIsSentinelUser] = useIsSentinelUser();
404 const isSecurityCheckupAvailable = useIsSecurityCheckupAvailable();
406 if (loadingIsSentinelUser) {
410 if (isSentinelUser || !isSecurityCheckupAvailable) {
412 <SentinelUserRecoveryCard
414 canDisplayNewSentinelSettings={canDisplayNewSentinelSettings}
415 isSentinelUser={isSentinelUser}
420 return <SecurityCheckupCard />;
423 export default RecoveryCard;