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';
6 selectCreatedItemsCount,
8 selectInAppNotificationsEnabled,
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,
29 if (previous) return false;
30 const state = store.getState();
31 const { waitingNewUserInvites } = selectUserState(state);
32 return (waitingNewUserInvites ?? 0) > 0;
36 export const createWelcomeRule = () =>
37 createOnboardingRule({
38 message: OnboardingMessage.WELCOME,
40 if (!DESKTOP_BUILD) return false;
45 export const createPermissionsRule = (checkPermissionsGranted: () => boolean) =>
46 createOnboardingRule({
47 message: OnboardingMessage.PERMISSIONS_REQUIRED,
48 when: () => !checkPermissionsGranted(),
51 export const createStorageIssueRule = (checkStorageFull: () => boolean) =>
52 createOnboardingRule({
53 message: OnboardingMessage.STORAGE_ISSUE,
54 when: () => checkStorageFull(),
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() } }),
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;
72 export const createTrialRule = (store: Store<State>) =>
73 createOnboardingRule({
74 message: OnboardingMessage.TRIAL,
76 const passPlan = selectPassPlan(store.getState());
77 return !previous && passPlan === UserPassPlan.TRIAL;
81 export const createB2BRule = (store: Store<State>) =>
82 createOnboardingRule({
83 message: OnboardingMessage.B2B_ONBOARDING,
85 const passPlan = selectPassPlan(store.getState());
86 return !previous && passPlan === UserPassPlan.BUSINESS;
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;
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;
114 export const createMonitorRule = () =>
115 createOnboardingRule({
116 message: OnboardingMessage.PASS_MONITOR,
117 when: (previous) => !previous,
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,
127 export const createAliasTrashConfirmRule = () =>
128 createOnboardingRule({
129 message: OnboardingMessage.ALIAS_TRASH_CONFIRM,
130 when: (previous) => !previous,
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);
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;
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) {
178 return !previous && selectFeatureFlag(PassFeature.PassBlackFriday2024Family)(state);
180 return !previous && selectFeatureFlag(PassFeature.PassBlackFriday2024Lifetime)(state);
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;