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(),
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),
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));
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);
63 const checkMessage = (message: OnboardingMessage): { enabled: boolean } => {
64 const rule = options.rules.find((rule) => rule.message === message);
65 return { enabled: rule ? checkRule(rule) : false };
68 /* Define extra rules in the `ONBOARDING_RULES` constant :
69 * we will resolve the first message that matches the rule's
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;
86 ...state.acknowledged.filter((data) => data.message !== message),
88 ...(acknowledged ?? {}),
90 acknowledgedOn: getEpoch(),
91 count: (acknowledged?.count ?? 0) + 1,
99 const init = async () => {
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');
111 return { acknowledge, checkMessage, init, reset, setState, getMessage, state };
114 export type OnboardingService = ReturnType<typeof createOnboardingService>;