Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / onboarding / service.ts
blob7ea2fc0fb39f7224041c1c34853e5eff23a7a06a
1 import type { AnyStorage } from '@proton/pass/types';
2 import { type Maybe, type OnboardingAcknowledgment, OnboardingMessage, type OnboardingState } 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 OnboardingServiceOptions<StorageKey extends string> = {
8     /** defines where onboarding data will be stored */
9     storage: AnyStorage<Record<StorageKey, string>>;
10     /** defines which onboarding rule this service supports  */
11     rules: OnboardingRule[];
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 OnboardingWhen = (previousAck: Maybe<OnboardingAcknowledgment>, state: OnboardingState) => boolean;
19 export type OnboardingAck = (ack: OnboardingAcknowledgment) => OnboardingAcknowledgment;
20 export type OnboardingRule = {
21     /** Onboarding message type */
22     message: OnboardingMessage;
23     /** Given any previous acknowledgments for this particular message and the
24      * current onboarding service state, should return a boolean flag indicating
25      * wether this rule should be triggered */
26     when?: OnboardingWhen;
27     /** Optional callback that will be executed when this particular onboarding
28      * message is acknowledged */
29     onAcknowledge?: OnboardingAck;
32 export const INITIAL_ONBOARDING_STATE: OnboardingState = {
33     installedOn: getEpoch(),
34     updatedOn: -1,
35     acknowledged: [],
38 export const createOnboardingRule = (options: OnboardingRule): OnboardingRule => ({
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 createOnboardingService = <StorageKey extends string>(options: OnboardingServiceOptions<StorageKey>) => {
46     const state: OnboardingState = { ...INITIAL_ONBOARDING_STATE };
48     /** Sets the onboarding service state and updates the storage */
49     const setState = (update: Partial<OnboardingState>) => {
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: OnboardingRule): 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: OnboardingMessage): { 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 onboarding message by either pushing
78      * it to the acknowledged messages list or updating the entry */
79     const acknowledge = (message: OnboardingMessage) => {
80         logger.info(`[Onboarding] Acknowledging "${OnboardingMessage[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 onboarding = await options.storage.getItem(key);
104             if (typeof onboarding === 'string') setState(JSON.parse(onboarding));
105             else throw Error('Onboarding data not found');
106         } catch {
107             setState(state);
108         }
109     };
111     return { acknowledge, checkMessage, init, reset, setState, getMessage, state };
114 export type OnboardingService = ReturnType<typeof createOnboardingService>;