Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / hooks / useLockSetup.tsx
blobb03ce21a856c41960368b2e50e9e4e584a5142de
1 import { useEffect, useMemo, useState } from 'react';
2 import { useSelector } from 'react-redux';
4 import { c } from 'ttag';
6 import { useNotifications } from '@proton/components';
7 import { useAuthStore } from '@proton/pass/components/Core/AuthStoreProvider';
8 import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider';
9 import { usePasswordUnlock } from '@proton/pass/components/Lock/PasswordUnlockProvider';
10 import { usePinUnlock } from '@proton/pass/components/Lock/PinUnlockProvider';
11 import { useUnlock } from '@proton/pass/components/Lock/UnlockProvider';
12 import { useOrganization } from '@proton/pass/components/Organization/OrganizationProvider';
13 import { useSpotlight } from '@proton/pass/components/Spotlight/SpotlightProvider';
14 import { DEFAULT_LOCK_TTL, UpsellRef } from '@proton/pass/constants';
15 import { useActionRequest } from '@proton/pass/hooks/useActionRequest';
16 import { LockMode } from '@proton/pass/lib/auth/lock/types';
17 import { isPaidPlan } from '@proton/pass/lib/user/user.predicates';
18 import { lockCreateIntent } from '@proton/pass/store/actions';
19 import { lockCreateRequest } from '@proton/pass/store/actions/requests';
20 import {
21     selectExtraPasswordEnabled,
22     selectLockMode,
23     selectLockTTL,
24     selectPassPlan,
25     selectUserSettings,
26 } from '@proton/pass/store/selectors';
27 import type { Maybe, MaybeNull } from '@proton/pass/types';
28 import { PASS_APP_NAME } from '@proton/shared/lib/constants';
29 import { SETTINGS_PASSWORD_MODE } from '@proton/shared/lib/interfaces';
30 import noop from '@proton/utils/noop';
32 type LockState = {
33     /** If `true`, user should not be able to toggle the TTL */
34     orgControlled: boolean;
35     /** Loading state for any ongoing lock mutation */
36     loading: boolean;
37     /** Current lock mode (Accounts for optimistic updates) */
38     mode: LockMode;
39     /** TTL Value is disabled when `orgControlled` or `LockMode.NONE` */
40     ttl: { value: Maybe<number>; disabled: boolean };
43 type BiometricsState = {
44     /** `true` if biometric already setup or on successful `canCheckPresence` */
45     enabled: boolean;
46     /** Biometrics is a paid only feature (disabled for free users) */
47     needsUpgrade: boolean;
50 type PasswordState = {
51     /** Password lock can be enabled if the user is not in two
52      * password mode OR if he has a valid offline password. */
53     enabled: boolean;
56 interface LockSetup {
57     lock: LockState;
58     biometrics: BiometricsState;
59     password: PasswordState;
60     setLockMode: (mode: LockMode) => Promise<void>;
61     setLockTTL: (ttl: number) => Promise<void>;
64 export const useLockSetup = (): LockSetup => {
65     const { getBiometricsKey } = usePassCore();
66     const { createNotification } = useNotifications();
68     const confirmPin = usePinUnlock();
69     const confirmPassword = usePasswordUnlock();
70     const spotlight = useSpotlight();
71     const authStore = useAuthStore();
73     const org = useOrganization({ sync: true });
74     const orgLockTTL = org?.settings.ForceLockSeconds;
76     const currentLockMode = useSelector(selectLockMode);
77     const hasExtraPassword = useSelector(selectExtraPasswordEnabled);
78     const lockTTL = useSelector(selectLockTTL);
79     const pwdMode = useSelector(selectUserSettings)?.Password?.Mode;
80     const plan = useSelector(selectPassPlan);
81     const isFreePlan = !isPaidPlan(plan);
83     /** When switching locks, the next lock might temporarily
84      * be set to `LockMode.NONE` before updating to the new lock.
85      * This temporary state change can cause flickering. To avoid
86      * this, we use an optimistic value for the next lock. */
87     const [nextLock, setNextLock] = useState<MaybeNull<{ ttl: number; mode: LockMode }>>(null);
88     const [biometricsEnabled, setBiometricsEnabled] = useState(currentLockMode === LockMode.BIOMETRICS);
90     const unlock = useUnlock((err) => createNotification({ type: 'error', text: err.message }));
92     const createLock = useActionRequest(lockCreateIntent, {
93         initialRequestId: lockCreateRequest(),
94         onStart: ({ data }) => setNextLock({ ttl: data.lock.ttl, mode: data.lock.mode }),
95         onFailure: () => setNextLock(null),
96         onSuccess: () => setNextLock(null),
97     });
99     const setLockMode = async (mode: LockMode) => {
100         if (isFreePlan && mode === LockMode.BIOMETRICS) {
101             return spotlight.setUpselling({
102                 type: 'pass-plus',
103                 upsellRef: UpsellRef.PASS_BIOMETRICS,
104             });
105         }
107         const ttl = orgLockTTL || (lockTTL ?? DEFAULT_LOCK_TTL);
108         /** If the current lock mode is a session lock - always
109          * ask for the current PIN in order to delete the lock */
110         const current = await new Promise<Maybe<{ secret: string }>>(async (resolve) => {
111             switch (currentLockMode) {
112                 case LockMode.SESSION:
113                     return confirmPin({
114                         title: c('Title').t`Confirm PIN code`,
115                         assistiveText: c('Info')
116                             .t`Please confirm your PIN code in order to unregister your current lock.`,
117                         onSubmit: async (secret) => {
118                             await unlock({ mode: currentLockMode, secret });
119                             resolve({ secret });
120                         },
121                     });
123                 case LockMode.BIOMETRICS: {
124                     /** Confirm the biometric key before proceeding */
125                     const secret = (await getBiometricsKey?.(authStore!).catch(noop)) ?? '';
126                     return unlock({ mode: currentLockMode, secret })
127                         .then(() => resolve({ secret }))
128                         .catch(() => resolve(undefined));
129                 }
131                 case LockMode.PASSWORD:
132                     return confirmPassword({
133                         message: (() => {
134                             switch (mode) {
135                                 /** If the next mode is `BIOMETRIC` then we'll feed the result of this
136                                  *  first unlock call to the `BIOMETRIC` lock creation */
137                                 case LockMode.BIOMETRICS:
138                                     return hasExtraPassword
139                                         ? c('Info')
140                                               .t`Please confirm your extra password in order to auto-lock with biometrics.`
141                                         : c('Info')
142                                               .t`Please confirm your password in order to auto-lock with biometrics.`;
143                                 default:
144                                     return hasExtraPassword
145                                         ? c('Info')
146                                               .t`Please confirm your extra password in order to unregister your current lock.`
147                                         : c('Info')
148                                               .t`Please confirm your password in order to unregister your current lock.`;
149                             }
150                         })(),
151                         onSubmit: async (secret) => {
152                             await unlock({ mode: currentLockMode, secret });
153                             resolve({ secret });
154                         },
155                     });
157                 default:
158                     return resolve(undefined);
159             }
160         });
162         switch (mode) {
163             case LockMode.SESSION:
164                 return confirmPin({
165                     title: c('Title').t`Create PIN code`,
166                     assistiveText: c('Info')
167                         .t`You will use this PIN to unlock ${PASS_APP_NAME} once it auto-locks after a period of inactivity.`,
168                     onSubmit: (secret) =>
169                         confirmPin({
170                             title: c('Title').t`Confirm PIN code`,
171                             assistiveText: c('Info')
172                                 .t`You will use this PIN to unlock ${PASS_APP_NAME} once it auto-locks after a period of inactivity.`,
173                             onSubmit: (confirmed) => {
174                                 if (confirmed === secret) createLock.dispatch({ mode, secret, ttl });
175                                 else createNotification({ type: 'error', text: c('Error').t`PIN codes do not match` });
176                             },
177                         }),
178                 });
180             case LockMode.PASSWORD:
181                 return confirmPassword({
182                     onSubmit: (secret) => createLock.dispatch({ mode, secret, ttl, current }),
183                     message: hasExtraPassword
184                         ? c('Info')
185                               .t`Please confirm your extra password in order to auto-lock with your extra password.`
186                         : c('Info').t`Please confirm your password in order to auto-lock with your password.`,
187                 });
189             case LockMode.BIOMETRICS: {
190                 if (currentLockMode === LockMode.PASSWORD) {
191                     /** if unregistered password lock then we have the secret already */
192                     return createLock.dispatch({ mode, secret: current?.secret ?? '', ttl });
193                 }
195                 /* else prompt for password */
196                 return confirmPassword({
197                     onSubmit: (secret) => createLock.dispatch({ mode, secret, ttl, current }),
198                     message: hasExtraPassword
199                         ? c('Info').t`Please confirm your extra password in order to auto-lock with biometrics.`
200                         : c('Info').t`Please confirm your password in order to auto-lock with biometrics.`,
201                 });
202             }
204             case LockMode.NONE:
205                 return createLock.dispatch({ mode, secret: '', ttl, current });
206         }
207     };
209     const setLockTTL = async (ttl: number) => {
210         switch (currentLockMode) {
211             case LockMode.SESSION:
212                 return confirmPin({
213                     onSubmit: (secret) =>
214                         createLock.dispatch({ mode: currentLockMode, secret, ttl, current: { secret } }),
215                     title: c('Title').t`Auto-lock update`,
216                     assistiveText: c('Info').t`Please confirm your PIN code to edit this setting.`,
217                 });
219             case LockMode.PASSWORD:
220             case LockMode.BIOMETRICS:
221                 return confirmPassword({
222                     onSubmit: (secret) => createLock.dispatch({ mode: currentLockMode, secret, ttl }),
223                     message: hasExtraPassword
224                         ? c('Info').t`Please confirm your extra password in order to update the auto-lock time.`
225                         : c('Info').t`Please confirm your password in order to update the auto-lock time.`,
226                 });
227         }
228     };
230     useEffect(() => {
231         /** Block reload/navigation if a lock request is on-going.
232          * Custom `beforeunload` messages are now deprecated */
233         const onBeforeUnload = (evt: BeforeUnloadEvent) => {
234             if (createLock.loading) {
235                 evt.preventDefault();
236                 evt.returnValue = '';
237                 return '';
238             }
239         };
241         window.addEventListener('beforeunload', onBeforeUnload);
242         return () => window.removeEventListener('beforeunload', onBeforeUnload);
243     }, [createLock.loading]);
245     useEffect(() => {
246         (async () => {
247             if (!DESKTOP_BUILD || currentLockMode === LockMode.BIOMETRICS) return;
248             const canCheckPresence = (await window.ctxBridge?.canCheckPresence?.()) ?? false;
249             setBiometricsEnabled(canCheckPresence);
250         })().catch(noop);
251     }, [currentLockMode]);
253     const lock = useMemo(
254         () => ({
255             orgControlled: Boolean(orgLockTTL),
256             loading: createLock.loading,
257             mode: nextLock?.mode ?? currentLockMode,
258             ttl: {
259                 value: nextLock?.ttl || orgLockTTL || lockTTL,
260                 disabled: Boolean(currentLockMode === LockMode.NONE || orgLockTTL),
261             },
262         }),
263         [currentLockMode, nextLock, orgLockTTL, lockTTL, createLock.loading]
264     );
266     const biometrics = useMemo(
267         () => ({ enabled: biometricsEnabled, needsUpgrade: isFreePlan }),
268         [biometricsEnabled, isFreePlan]
269     );
271     const password = useMemo(
272         () => ({
273             enabled:
274                 !EXTENSION_BUILD &&
275                 (pwdMode !== SETTINGS_PASSWORD_MODE.TWO_PASSWORD_MODE || (authStore?.hasOfflinePassword() ?? false)),
276         }),
277         [pwdMode]
278     );
280     return {
281         lock,
282         biometrics,
283         password,
284         setLockMode,
285         setLockTTL,
286     };