Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / store / sagas / events / channel.user.ts
blobd939d38f2e0bada746574c0125ce06dc27a3e0ad
1 /* eslint-disable @typescript-eslint/no-throw-literal, curly */
2 import { all, fork, put, select } from 'redux-saga/effects';
4 import { PassCrypto } from '@proton/pass/lib/crypto';
5 import type { EventCursor, EventManagerEvent } from '@proton/pass/lib/events/manager';
6 import { getUserAccessIntent, getUserFeaturesIntent, syncIntent, userEvent } from '@proton/pass/store/actions';
7 import { getOrganizationSettings } from '@proton/pass/store/actions/creators/organization';
8 import { withRevalidate } from '@proton/pass/store/request/enhancers';
9 import { SyncType } from '@proton/pass/store/sagas/client/sync';
10 import { selectAllAddresses, selectLatestEventId, selectUserSettings } from '@proton/pass/store/selectors';
11 import type { RootSagaOptions } from '@proton/pass/store/types';
12 import type { MaybeNull, UserEvent } from '@proton/pass/types';
13 import { type Api } from '@proton/pass/types';
14 import { prop } from '@proton/pass/utils/fp/lens';
15 import { notIn } from '@proton/pass/utils/fp/predicates';
16 import { logId, logger } from '@proton/pass/utils/logger';
17 import { getEvents, getLatestID } from '@proton/shared/lib/api/events';
18 import type { Address, UserSettings } from '@proton/shared/lib/interfaces';
19 import identity from '@proton/utils/identity';
21 import { eventChannelFactory } from './channel.factory';
22 import { channelEventsWorker, channelInitWorker } from './channel.worker';
23 import type { EventChannel } from './types';
25 function* onUserEvent(
26     event: EventManagerEvent<UserEvent>,
27     _: EventChannel<UserEvent>,
28     { getAuthStore, getTelemetry, onLocaleUpdated }: RootSagaOptions
29 ) {
30     const telemetry = getTelemetry();
31     if ('error' in event) throw event.error;
33     const currentEventId = (yield select(selectLatestEventId)) as MaybeNull<string>;
34     const userId = getAuthStore().getUserID()!;
35     const userSettings: MaybeNull<UserSettings> = yield select(selectUserSettings);
37     /* dispatch only if there was a change */
38     if (currentEventId !== event.EventID) {
39         yield put(userEvent(event));
40         logger.info(`[ServerEvents::User] event ${logId(event.EventID!)}`);
41     }
43     const { User: user } = event;
45     if (event.UserSettings && telemetry) {
46         const { Telemetry } = event.UserSettings;
47         if (Telemetry !== userSettings?.Telemetry) telemetry[Telemetry === 1 ? 'start' : 'stop']();
48     }
50     if (event.UserSettings?.Locale) {
51         const { Locale } = event.UserSettings;
52         if (Locale !== userSettings?.Locale) yield onLocaleUpdated?.(Locale);
53     }
55     /* if we get the user model from the event, check if
56      * any new active user keys are available. We might be
57      * dealing with a user re-activating a disabled user key
58      * in which case we want to trigger a full data sync in
59      * order to access any previously inactive shares */
60     if (user) {
61         const localUserKeyIds = (PassCrypto.getContext().userKeys ?? []).map(prop('ID'));
62         const activeUserKeys = user.Keys.filter(({ Active }) => Active === 1);
64         const keysUpdated =
65             activeUserKeys.length !== localUserKeyIds.length ||
66             activeUserKeys.some(({ ID }) => notIn(localUserKeyIds)(ID));
68         if (keysUpdated) {
69             logger.info(`[ServerEvents::User] Detected user keys update`);
70             const keyPassword = getAuthStore().getPassword() ?? '';
71             const addresses = (yield select(selectAllAddresses)) as Address[];
72             yield PassCrypto.hydrate({ user, keyPassword, addresses, clear: false });
73             yield put(syncIntent(SyncType.FULL)); /* trigger a full data sync */
74         }
75     }
77     /* if the subscription/invoice changes, refetch the user Plan and check Organization */
78     const revalidateUserAccess = event.Subscription || event.Invoices;
80     /* Synchronize user access, feature flags & organization whenever polling
81      * for core user events. These actions are throttled via `maxAge` metadata */
82     yield put((revalidateUserAccess ? withRevalidate : identity)(getUserAccessIntent(userId)));
83     yield put(getUserFeaturesIntent(userId));
84     yield put(getOrganizationSettings.intent());
87 export const createUserChannel = (api: Api, eventID: string) =>
88     eventChannelFactory<UserEvent>({
89         api,
90         channelId: 'user',
91         initialEventID: eventID,
92         query: getEvents,
93         getCursor: ({ EventID, More }) => ({ EventID, More: Boolean(More) }),
94         getLatestEventID: () => api<EventCursor>(getLatestID()).then(({ EventID }) => EventID),
95         onEvent: onUserEvent,
96         onClose: () => logger.info(`[ServerEvents::User] closing channel`),
97     });
99 export function* userChannel(api: Api, options: RootSagaOptions) {
100     logger.info(`[ServerEvents::User] start polling for user events`);
102     const eventID: string = ((yield select(selectLatestEventId)) as ReturnType<typeof selectLatestEventId>) ?? '';
103     const eventsChannel = createUserChannel(api, eventID);
104     const events = fork(channelEventsWorker<UserEvent>, eventsChannel, options);
105     const wakeup = fork(channelInitWorker<UserEvent>, eventsChannel, options);
107     yield all([events, wakeup]);