Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / onboarding / rules.ts
bloba6ec32f785b52132ef64e116eb1429abdd3d4380
1 import type { Store } from 'redux';
3 import { ITEM_COUNT_RATING_PROMPT, PASS_BF_2024_DATES } from '@proton/pass/constants';
4 import { api } from '@proton/pass/lib/api/api';
5 import {
6     selectCreatedItemsCount,
7     selectFeatureFlag,
8     selectInAppNotificationsEnabled,
9     selectLockEnabled,
10     selectPassPlan,
11     selectUserData,
12     selectUserPlan,
13     selectUserState,
14 } from '@proton/pass/store/selectors';
15 import type { State } from '@proton/pass/store/types';
16 import { type Maybe, type MaybeNull, OnboardingMessage, PlanType } from '@proton/pass/types';
17 import { PassFeature } from '@proton/pass/types/api/features';
18 import { UserPassPlan } from '@proton/pass/types/api/plan';
19 import { merge } from '@proton/pass/utils/object/merge';
20 import { UNIX_DAY, UNIX_MONTH, UNIX_WEEK } from '@proton/pass/utils/time/constants';
21 import { getEpoch } from '@proton/pass/utils/time/epoch';
23 import { createOnboardingRule } from './service';
25 export const createPendingShareAccessRule = (store: Store<State>) =>
26     createOnboardingRule({
27         message: OnboardingMessage.PENDING_SHARE_ACCESS,
28         when: (previous) => {
29             if (previous) return false;
30             const state = store.getState();
31             const { waitingNewUserInvites } = selectUserState(state);
32             return (waitingNewUserInvites ?? 0) > 0;
33         },
34     });
36 export const createWelcomeRule = () =>
37     createOnboardingRule({
38         message: OnboardingMessage.WELCOME,
39         when: (previous) => {
40             if (!DESKTOP_BUILD) return false;
41             return !previous;
42         },
43     });
45 export const createPermissionsRule = (checkPermissionsGranted: () => boolean) =>
46     createOnboardingRule({
47         message: OnboardingMessage.PERMISSIONS_REQUIRED,
48         when: () => !checkPermissionsGranted(),
49     });
51 export const createStorageIssueRule = (checkStorageFull: () => boolean) =>
52     createOnboardingRule({
53         message: OnboardingMessage.STORAGE_ISSUE,
54         when: () => checkStorageFull(),
55     });
57 export const createUpdateRule = (getAvailableUpdate: () => MaybeNull<string>) =>
58     createOnboardingRule({
59         message: OnboardingMessage.UPDATE_AVAILABLE,
60         /* keep a reference to the current available update so as
61          * not to re-prompt the user with this update if ignored */
62         onAcknowledge: (ack) => merge(ack, { extraData: { version: getAvailableUpdate() } }),
63         when: (previous) => {
64             const availableVersion = getAvailableUpdate();
65             const previousAckVersion = previous?.extraData?.version as MaybeNull<Maybe<string>>;
66             const shouldPrompt = !previous || previousAckVersion !== availableVersion;
68             return availableVersion !== null && shouldPrompt;
69         },
70     });
72 export const createTrialRule = (store: Store<State>) =>
73     createOnboardingRule({
74         message: OnboardingMessage.TRIAL,
75         when: (previous) => {
76             const passPlan = selectPassPlan(store.getState());
77             return !previous && passPlan === UserPassPlan.TRIAL;
78         },
79     });
81 export const createB2BRule = (store: Store<State>) =>
82     createOnboardingRule({
83         message: OnboardingMessage.B2B_ONBOARDING,
84         when: (previous) => {
85             const passPlan = selectPassPlan(store.getState());
86             return !previous && passPlan === UserPassPlan.BUSINESS;
87         },
88     });
90 export const createSecurityRule = (store: Store<State>) =>
91     createOnboardingRule({
92         message: OnboardingMessage.SECURE_EXTENSION,
93         when: (previous, { installedOn }) => {
94             /* should prompt only if user has no extension lock AND
95              * message was not previously acknowledged AND user has
96              * installed at least one day ago */
97             const now = getEpoch();
98             const lockEnabled = selectLockEnabled(store.getState());
99             const shouldPrompt = !previous && now - installedOn > UNIX_DAY;
101             return !lockEnabled && shouldPrompt;
102         },
103     });
105 export const createUserRatingRule = (store: Store<State>) =>
106     createOnboardingRule({
107         message: OnboardingMessage.USER_RATING,
108         when: (previous) => {
109             const createdItemsCount = selectCreatedItemsCount(store.getState());
110             return !previous && createdItemsCount >= ITEM_COUNT_RATING_PROMPT;
111         },
112     });
114 export const createMonitorRule = () =>
115     createOnboardingRule({
116         message: OnboardingMessage.PASS_MONITOR,
117         when: (previous) => !previous,
118     });
120 export const createMonitorLearnMoreRule = () =>
121     createOnboardingRule({
122         message: OnboardingMessage.PASS_MONITOR_LEARN_MORE,
123         onAcknowledge: (ack) => merge(ack, { extraData: { expanded: !ack.extraData?.expanded } }),
124         when: (previous) => !previous || !previous.extraData?.expanded,
125     });
127 export const createAliasTrashConfirmRule = () =>
128     createOnboardingRule({
129         message: OnboardingMessage.ALIAS_TRASH_CONFIRM,
130         when: (previous) => !previous,
131     });
133 export const createFamilyPlanPromo2024Rule = (store: Store<State>) =>
134     createOnboardingRule({
135         message: OnboardingMessage.FAMILY_PLAN_PROMO_2024,
136         when: (previous) => {
137             if (previous) return false;
139             const state = store.getState();
140             if (!selectInAppNotificationsEnabled(state)) return false;
142             const enabled = selectFeatureFlag(PassFeature.PassFamilyPlanPromo2024)(state);
143             const plan = selectUserPlan(state);
144             const cohortPass2023 = plan?.InternalName === 'pass2023';
145             const cohortPassFree = plan?.InternalName === 'free';
147             return enabled && (cohortPass2023 || cohortPassFree);
148         },
149     });
151 export const createAliasSyncEnableRule = (store: Store<State>) =>
152     createOnboardingRule({
153         message: OnboardingMessage.ALIAS_SYNC_ENABLE,
154         when: (previous) => {
155             const state = store.getState();
156             const enabled = selectFeatureFlag(PassFeature.PassSimpleLoginAliasesSync)(state);
157             const { pendingAliasToSync } = selectUserData(state);
159             return enabled && !previous && pendingAliasToSync > 0;
160         },
161     });
163 /* - Pass Family offer shown to pass2023 users
164  * - Pass Lifetime offer shown to free users */
165 export const createBlackFriday2024Rule = (store: Store<State>) =>
166     createOnboardingRule({
167         message: OnboardingMessage.BLACK_FRIDAY_2024,
168         when: (previous) => {
169             const state = store.getState();
170             if (!selectInAppNotificationsEnabled(state)) return false;
172             /* sanity check in-case feature flags are unreliable */
173             const now = api.getState().serverTime?.getTime() ?? Date.now();
174             if (now > PASS_BF_2024_DATES[1]) return false;
176             switch (selectUserPlan(state)?.InternalName) {
177                 case 'pass2023':
178                     return !previous && selectFeatureFlag(PassFeature.PassBlackFriday2024Family)(state);
179                 case 'free':
180                     return !previous && selectFeatureFlag(PassFeature.PassBlackFriday2024Lifetime)(state);
181                 default:
182                     return false;
183             }
184         },
185     });
187 export const createUserRenewalRule = (store: Store<State>) =>
188     createOnboardingRule({
189         message: OnboardingMessage.USER_RENEWAL,
190         when: (previous) => {
191             const plan = selectUserPlan(store.getState());
193             // Do not show if it's not a Plus plan or can't manage subscription or is already renewing
194             if (plan?.Type !== PlanType.plus || !plan?.ManageSubscription || plan?.SubscriptionRenewal) return false;
196             const now = getEpoch();
198             // If acknowledged, show it again after two weeks
199             if (previous) return now - previous.acknowledgedOn > UNIX_WEEK * 2;
201             const subscriptionEnd = plan?.SubscriptionEnd ?? 0;
203             // Prevent showing the banner if the plan type has not been
204             // updated to 'Free' after the subscription date has passed
205             if (subscriptionEnd > now) return false;
207             return subscriptionEnd - now < UNIX_MONTH;
208         },
209     });