Use source loader for email sprite icons
[ProtonMail-WebClient.git] / packages / account / securityCheckup / listener.ts
blob316efd2705e77c3d09121abaf8190b3f28d47312
1 import type { SharedStartListening } from '@proton/redux-shared-store/listenerInterface';
2 import { TelemetryAccountSecurityCheckupEvents, TelemetryMeasurementGroups } from '@proton/shared/lib/api/telemetry';
3 import { SECURITY_CHECKUP_PATHS } from '@proton/shared/lib/constants';
4 import { sendTelemetryReport } from '@proton/shared/lib/helpers/metrics';
5 import {
6     getIsPerfectDeviceRecoveryState,
7     getIsPerfectEmailState,
8     getIsPerfectPhoneState,
9     getIsPerfectPhraseState,
10 } from '@proton/shared/lib/helpers/securityCheckup';
11 import { MNEMONIC_STATUS, SETTINGS_STATUS } from '@proton/shared/lib/interfaces';
12 import SecurityCheckupCohort from '@proton/shared/lib/interfaces/securityCheckup/SecurityCheckupCohort';
13 import type SecurityState from '@proton/shared/lib/interfaces/securityCheckup/SecurityState';
14 import { getIsMnemonicAvailable } from '@proton/shared/lib/mnemonic';
15 import { getIsRecoveryFileAvailable } from '@proton/shared/lib/recoveryFile/recoveryFile';
17 import type { AddressesState } from '../addresses';
18 import { selectAddresses } from '../addresses';
19 import type { UserState } from '../user';
20 import { selectUser } from '../user';
21 import type { UserKeysState } from '../userKeys';
22 import { selectUserKeys } from '../userKeys';
23 import type { UserSettingsState } from '../userSettings';
24 import { selectUserSettings } from '../userSettings';
25 import getSource from './helpers/getSource';
26 import getValidSecurityCheckupSession from './helpers/getValidSecurityCheckupSession';
27 import {
28     removeSecurityCheckupSessionItem,
29     setSecurityCheckupSessionItem,
30 } from './helpers/securityCheckupSessionStorage';
31 import type { SecurityCheckupReduxState } from './slice';
32 import { securityCheckupSlice, selectSecurityCheckup } from './slice';
34 interface RequiredState
35     extends UserSettingsState,
36         UserState,
37         AddressesState,
38         UserKeysState,
39         SecurityCheckupReduxState {}
41 export const securityCheckupListener = (startListening: SharedStartListening<RequiredState>) => {
42     /**
43      * Calculate security state
44      */
45     startListening({
46         predicate: (action, currentState, previousState) => {
47             const previousUser = selectUser(previousState);
48             const currentUser = selectUser(currentState);
50             const previousUserKeys = selectUserKeys(previousState);
51             const currentUserKeys = selectUserKeys(currentState);
53             const previousAddresses = selectAddresses(previousState).value;
54             const currentAddresses = selectAddresses(currentState).value;
56             const previousUserSettings = selectUserSettings(previousState);
57             const currentUserSettings = selectUserSettings(currentState);
59             return (
60                 currentUser !== previousUser ||
61                 currentUserKeys !== previousUserKeys ||
62                 currentAddresses !== previousAddresses ||
63                 currentUserSettings !== previousUserSettings
64             );
65         },
66         effect: async (action, listenerApi) => {
67             const {
68                 getState,
69                 dispatch,
70                 extra: { config },
71             } = listenerApi;
73             const { user, userKeys, addresses, userSettings, securityCheckup } = getState();
74             const { actions } = securityCheckupSlice;
76             if (!user.value || !userKeys.value || !addresses.value || !userSettings.value) {
77                 if (!securityCheckup.loading) {
78                     dispatch(actions.setLoading({ loading: true }));
79                 }
80                 return;
81             }
83             const isMnemonicAvailable = getIsMnemonicAvailable({
84                 addresses: addresses.value,
85                 user: user.value,
86                 app: config.APP_NAME,
87             });
88             const isRecoveryFileAvailable = getIsRecoveryFileAvailable({
89                 user: user.value,
90                 addresses: addresses.value,
91                 userKeys: userKeys.value,
92                 appName: config.APP_NAME,
93             });
95             const securityState: SecurityState = {
96                 phrase: {
97                     isAvailable: isMnemonicAvailable,
98                     isSet: user.value.MnemonicStatus === MNEMONIC_STATUS.SET,
99                     isOutdated: user.value.MnemonicStatus === MNEMONIC_STATUS.OUTDATED,
100                 },
101                 email: {
102                     value: userSettings.value.Email.Value,
103                     isEnabled: !!userSettings.value.Email.Reset,
104                     verified: userSettings.value.Email.Status === SETTINGS_STATUS.VERIFIED,
105                 },
106                 phone: {
107                     value: userSettings.value.Phone.Value,
108                     isEnabled: !!userSettings.value.Phone.Reset,
109                     verified: userSettings.value.Phone.Status === SETTINGS_STATUS.VERIFIED,
110                 },
111                 deviceRecovery: {
112                     isAvailable: isRecoveryFileAvailable,
113                     isEnabled: !!userSettings.value.DeviceRecovery,
114                 },
115             };
117             dispatch(actions.setSecurityState({ securityState }));
119             if (securityCheckup.loading) {
120                 dispatch(actions.setLoading({ loading: false }));
121             }
122         },
123     });
125     /**
126      * Persist session in session storage on session change
127      */
128     startListening({
129         predicate: (action, currentState, previousState) => {
130             const previousSecurityCheckup = selectSecurityCheckup(previousState);
131             const currentSecurityCheckup = selectSecurityCheckup(currentState);
133             return previousSecurityCheckup.session !== currentSecurityCheckup.session;
134         },
135         effect: async (action, listenerApi) => {
136             const { getState } = listenerApi;
137             const { user, securityCheckup } = getState();
139             if (!user.value || !securityCheckup.session) {
140                 return;
141             }
143             setSecurityCheckupSessionItem(securityCheckup.session, user.value.ID);
144         },
145     });
147     /**
148      * Remove security checkup session from session storage on clear dispatch
149      */
150     startListening({
151         predicate: (action) => {
152             return securityCheckupSlice.actions.clearSession.match(action);
153         },
154         effect: async (action, listenerApi) => {
155             const { getState } = listenerApi;
156             const { user } = getState();
158             if (!user.value) {
159                 return;
160             }
162             removeSecurityCheckupSessionItem(user.value.ID);
163         },
164     });
166     /**
167      * Send telemetry on cohort transition
168      */
169     startListening({
170         predicate: (action, currentState, previousState) => {
171             const previousSecurityCheckup = selectSecurityCheckup(previousState);
172             const currentSecurityCheckup = selectSecurityCheckup(currentState);
174             return (
175                 previousSecurityCheckup.cohort !== undefined &&
176                 currentSecurityCheckup.cohort !== undefined &&
177                 previousSecurityCheckup.cohort !== currentSecurityCheckup.cohort
178             );
179         },
180         effect: async (action, listenerApi) => {
181             // Only send telemetry if we are in the security checkup
182             // The Cohort can change outside the security checkup. Ie enabling the recovery phrase on the recovery page
183             if (!listenerApi.extra.history.location.pathname.includes(SECURITY_CHECKUP_PATHS.ROOT)) {
184                 return;
185             }
187             const { user, securityCheckup } = listenerApi.getState();
189             if (!user.value) {
190                 return;
191             }
193             const { cohort, session, securityState } = securityCheckup;
194             if (!cohort || cohort === SecurityCheckupCohort.NO_RECOVERY_METHOD) {
195                 // No change should have occurred
196                 return;
197             }
199             const securityCheckupSession = getValidSecurityCheckupSession({
200                 currentSession: session,
201                 currentCohort: cohort,
202             });
204             const isPerfectPhraseState = getIsPerfectPhraseState(securityState);
205             const isPerfectEmailState = getIsPerfectEmailState(securityState);
206             const isPerfectPhoneState = getIsPerfectPhoneState(securityState);
207             const isPerfectDeviceState = getIsPerfectDeviceRecoveryState(securityState);
208             const singleMethod = (() => {
209                 if (isPerfectPhraseState) {
210                     return 'phrase';
211                 }
213                 if (isPerfectEmailState && isPerfectDeviceState) {
214                     return 'email';
215                 }
217                 if (isPerfectPhoneState && isPerfectDeviceState) {
218                     return 'phone';
219                 }
221                 return 'unknown';
222             })();
224             const {
225                 event,
226                 dimensions = {},
227             }: { event?: TelemetryAccountSecurityCheckupEvents; dimensions?: Record<string, string> } = (() => {
228                 if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_MULTIPLE) {
229                     return { event: TelemetryAccountSecurityCheckupEvents.completeRecoveryMultiple };
230                 }
232                 if (cohort === SecurityCheckupCohort.COMPLETE_RECOVERY_SINGLE) {
233                     return {
234                         event: TelemetryAccountSecurityCheckupEvents.completeRecoverySingle,
235                         dimensions: {
236                             singleMethod,
237                         },
238                     };
239                 }
241                 if (cohort === SecurityCheckupCohort.ACCOUNT_RECOVERY_ENABLED) {
242                     return { event: TelemetryAccountSecurityCheckupEvents.accountRecoveryEnabled };
243                 }
245                 return { event: undefined };
246             })();
248             if (!event) {
249                 return;
250             }
252             void sendTelemetryReport({
253                 api: listenerApi.extra.api,
254                 measurementGroup: TelemetryMeasurementGroups.accountSecurityCheckup,
255                 event,
256                 dimensions: {
257                     initialCohort: securityCheckupSession.initialCohort,
258                     ...dimensions,
259                 },
260             });
261         },
262     });
264     /**
265      * Get touchpoint source
266      */
267     startListening({
268         predicate: (_, currentState) => {
269             const currentSecurityCheckup = selectSecurityCheckup(currentState);
271             return !currentSecurityCheckup.source;
272         },
273         effect: async (action, listenerApi) => {
274             // Only calculate source if we are in the security checkup
275             if (!listenerApi.extra.history.location.pathname.includes(SECURITY_CHECKUP_PATHS.ROOT)) {
276                 return;
277             }
279             const { pathname, search } = listenerApi.extra.history.location;
280             const source = getSource({ pathname, search: new URLSearchParams(search) });
281             if (!source) {
282                 return;
283             }
285             listenerApi.dispatch(securityCheckupSlice.actions.setSource({ source }));
286         },
287     });