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 */
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(),
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),
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));
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);
63 const checkMessage = (message: SpotlightMessage): { 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 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;
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 data = await options.storage.getItem(key);
104 if (typeof data === 'string') setState(JSON.parse(data));
105 else throw Error('Spotlight data not found');
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>;