Merge branch 'IDTEAM-1.26.0' into 'main'
[ProtonMail-WebClient.git] / packages / pass / lib / spotlight / service.ts
blobaef855dbd14fbd40a966adabcf5e9da8f116faeb
1 import type { AnyStorage, MaybePromise } from '@proton/pass/types';
2 import { type Maybe, type SpotlightAcknowledgment, SpotlightMessage, type SpotlightState } from '@proton/pass/types';
3 import { logger } from '@proton/pass/utils/logger';
4 import { getEpoch } from '@proton/pass/utils/time/epoch';
5 import identity from '@proton/utils/identity';
7 type SpotlightServiceOptions<StorageKey extends string> = {
8     /** defines where spotlight data will be stored */
9     storage: AnyStorage<Record<StorageKey, string>>;
10     /** defines which spotlight rule this service supports  */
11     rules: SpotlightRule[];
12     /** resolves the storage key for the current user */
13     getStorageKey: () => StorageKey;
14     /** triggered on OnboardingService::init before hydration */
15     migrate?: (storageKey: StorageKey) => void;
18 export type SpotlightWhen = (previousAck: Maybe<SpotlightAcknowledgment>, state: SpotlightState) => boolean;
19 export type SpotlightAck = (ack: SpotlightAcknowledgment) => SpotlightAcknowledgment;
20 export type SpotlightRule = {
21     /** Onboarding message type */
22     message: SpotlightMessage;
23     /** Given any previous acknowledgments for this particular message and the
24      * current spotlight service state, should return a boolean flag indicating
25      * wether this rule should be triggered */
26     when?: SpotlightWhen;
27     /** Optional callback that will be executed when this particular spotlight
28      * message is acknowledged */
29     onAcknowledge?: SpotlightAck;
32 export const INITIAL_SPOTLIGHT_STATE: SpotlightState = {
33     installedOn: getEpoch(),
34     updatedOn: -1,
35     acknowledged: [],
38 export const createSpotlightRule = (options: SpotlightRule): SpotlightRule => ({
39     message: options.message,
40     onAcknowledge: options.onAcknowledge,
41     when: (previousAck, state) =>
42         options.when?.(previousAck, state) ?? !state.acknowledged.some((data) => data.message === options.message),
43 });
45 export const createSpotlightService = <StorageKey extends string>(options: SpotlightServiceOptions<StorageKey>) => {
46     const state: SpotlightState = { ...INITIAL_SPOTLIGHT_STATE };
48     /** Sets the spotlight service state and updates the storage */
49     const setState = (update: Partial<SpotlightState>) => {
50         state.acknowledged = update.acknowledged ?? state.acknowledged;
51         state.installedOn = update.installedOn ?? state.installedOn;
52         state.updatedOn = update.updatedOn ?? state.updatedOn;
53         void options.storage.setItem(options.getStorageKey(), JSON.stringify(state));
54     };
56     const checkRule = (rule: SpotlightRule): boolean => {
57         if (!rule.when) return true;
59         const ack = state.acknowledged.find((ack) => rule.message === ack.message);
60         return rule.when(ack, state);
61     };
63     const checkMessage = (message: SpotlightMessage): { enabled: boolean } => {
64         const rule = options.rules.find((rule) => rule.message === message);
65         return { enabled: rule ? checkRule(rule) : false };
66     };
68     /* Define extra rules in the `ONBOARDING_RULES` constant :
69      * we will resolve the first message that matches the rule's
70      * `when` condition */
71     const getMessage = () => ({ message: options.rules.find(checkRule)?.message ?? null });
73     /** Resets the state's acknowledged message list. This may be
74      * useful when logging out a user - preserves timestamps */
75     const reset = () => setState({ acknowledged: [] });
77     /** Acknowledges the given spotlight message by either pushing
78      * it to the acknowledged messages list or updating the entry */
79     const acknowledge = (message: SpotlightMessage) => {
80         logger.info(`[Onboarding] Acknowledging "${SpotlightMessage[message]}"`);
81         const acknowledged = state.acknowledged.find((data) => data.message === message);
82         const onAcknowledge = options.rules.find((rule) => rule.message === message)?.onAcknowledge ?? identity;
84         setState({
85             acknowledged: [
86                 ...state.acknowledged.filter((data) => data.message !== message),
87                 onAcknowledge({
88                     ...(acknowledged ?? {}),
89                     message,
90                     acknowledgedOn: getEpoch(),
91                     count: (acknowledged?.count ?? 0) + 1,
92                 }),
93             ],
94         });
96         return true;
97     };
99     const init = async () => {
100         try {
101             const key = options.getStorageKey();
102             options.migrate?.(key);
103             const data = await options.storage.getItem(key);
104             if (typeof data === 'string') setState(JSON.parse(data));
105             else throw Error('Spotlight data not found');
106         } catch {
107             setState(state);
108         }
109     };
111     return { acknowledge, checkMessage, init, reset, setState, getMessage, state };
114 export type SpotlightService = ReturnType<typeof createSpotlightService>;
116 export type SpotlightProxy = {
117     /** Acknowledge a spotlight message */
118     check: (message: SpotlightMessage) => MaybePromise<boolean>;
119     /** Returns `true` if a spotlight message should show */
120     acknowledge: (message: SpotlightMessage) => MaybePromise<boolean>;