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';
21 selectExtraPasswordEnabled,
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';
33 /** If `true`, user should not be able to toggle the TTL */
34 orgControlled: boolean;
35 /** Loading state for any ongoing lock mutation */
37 /** Current lock mode (Accounts for optimistic updates) */
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` */
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. */
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),
99 const setLockMode = async (mode: LockMode) => {
100 if (isFreePlan && mode === LockMode.BIOMETRICS) {
101 return spotlight.setUpselling({
103 upsellRef: UpsellRef.PASS_BIOMETRICS,
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:
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 });
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));
131 case LockMode.PASSWORD:
132 return confirmPassword({
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
140 .t`Please confirm your extra password in order to auto-lock with biometrics.`
142 .t`Please confirm your password in order to auto-lock with biometrics.`;
144 return hasExtraPassword
146 .t`Please confirm your extra password in order to unregister your current lock.`
148 .t`Please confirm your password in order to unregister your current lock.`;
151 onSubmit: async (secret) => {
152 await unlock({ mode: currentLockMode, secret });
158 return resolve(undefined);
163 case LockMode.SESSION:
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) =>
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` });
180 case LockMode.PASSWORD:
181 return confirmPassword({
182 onSubmit: (secret) => createLock.dispatch({ mode, secret, ttl, current }),
183 message: hasExtraPassword
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.`,
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 });
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.`,
205 return createLock.dispatch({ mode, secret: '', ttl, current });
209 const setLockTTL = async (ttl: number) => {
210 switch (currentLockMode) {
211 case LockMode.SESSION:
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.`,
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.`,
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 = '';
241 window.addEventListener('beforeunload', onBeforeUnload);
242 return () => window.removeEventListener('beforeunload', onBeforeUnload);
243 }, [createLock.loading]);
247 if (!DESKTOP_BUILD || currentLockMode === LockMode.BIOMETRICS) return;
248 const canCheckPresence = (await window.ctxBridge?.canCheckPresence?.()) ?? false;
249 setBiometricsEnabled(canCheckPresence);
251 }, [currentLockMode]);
253 const lock = useMemo(
255 orgControlled: Boolean(orgLockTTL),
256 loading: createLock.loading,
257 mode: nextLock?.mode ?? currentLockMode,
259 value: nextLock?.ttl || orgLockTTL || lockTTL,
260 disabled: Boolean(currentLockMode === LockMode.NONE || orgLockTTL),
263 [currentLockMode, nextLock, orgLockTTL, lockTTL, createLock.loading]
266 const biometrics = useMemo(
267 () => ({ enabled: biometricsEnabled, needsUpgrade: isFreePlan }),
268 [biometricsEnabled, isFreePlan]
271 const password = useMemo(
275 (pwdMode !== SETTINGS_PASSWORD_MODE.TWO_PASSWORD_MODE || (authStore?.hasOfflinePassword() ?? false)),