diff --git a/applications/pass-extension/src/app/content/injections/apps/dropdown/components/DropdownSwitch.tsx b/applications/pass-extension/src/app/content/injections/apps/dropdown/components/DropdownSwitch.tsx index d4403799c91..1a854b75607 100644 --- a/applications/pass-extension/src/app/content/injections/apps/dropdown/components/DropdownSwitch.tsx +++ b/applications/pass-extension/src/app/content/injections/apps/dropdown/components/DropdownSwitch.tsx @@ -1,4 +1,4 @@ -import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { forwardRef, type ForwardRefRenderFunction } from 'react'; import { AliasAutoSuggest } from 'proton-pass-extension/app/content/injections/apps/dropdown/views/AliasAutoSuggest'; import { ItemsList } from 'proton-pass-extension/app/content/injections/apps/dropdown/views/ItemsList'; diff --git a/applications/pass-extension/src/app/content/injections/apps/dropdown/views/AliasAutoSuggest.tsx b/applications/pass-extension/src/app/content/injections/apps/dropdown/views/AliasAutoSuggest.tsx index 220737e57cc..37ff5faaafe 100644 --- a/applications/pass-extension/src/app/content/injections/apps/dropdown/views/AliasAutoSuggest.tsx +++ b/applications/pass-extension/src/app/content/injections/apps/dropdown/views/AliasAutoSuggest.tsx @@ -11,6 +11,7 @@ import { c } from 'ttag'; import { CircleLoader } from '@proton/atoms/CircleLoader'; import { AliasPreview } from '@proton/pass/components/Alias/legacy/Alias.preview'; import { SubTheme } from '@proton/pass/components/Layout/Theme/types'; +import { UpsellRef } from '@proton/pass/constants'; import { useEnsureMounted } from '@proton/pass/hooks/useEnsureMounted'; import { useNavigateToUpgrade } from '@proton/pass/hooks/useNavigateToUpgrade'; import { contentScriptMessage, sendMessage } from '@proton/pass/lib/extension/message'; @@ -37,7 +38,7 @@ const getInitialLoadingText = (): string => c('Info').t`Generating alias...`; export const AliasAutoSuggest: VFC = ({ hostname, prefix, visible, onClose, onMessage }) => { const ensureMounted = useEnsureMounted(); - const navigateToUpgrade = useNavigateToUpgrade(); + const navigateToUpgrade = useNavigateToUpgrade({ upsellRef: UpsellRef.LIMIT_ALIAS }); const { userEmail } = useIFrameContext(); const [aliasOptions, setAliasOptions] = useState>(null); const [needsUpgrade, setNeedsUpgrade] = useState(false); diff --git a/applications/pass-extension/src/app/content/injections/apps/dropdown/views/ItemsList.tsx b/applications/pass-extension/src/app/content/injections/apps/dropdown/views/ItemsList.tsx index 2cbdb9fa0ae..d779fdbe846 100644 --- a/applications/pass-extension/src/app/content/injections/apps/dropdown/views/ItemsList.tsx +++ b/applications/pass-extension/src/app/content/injections/apps/dropdown/views/ItemsList.tsx @@ -10,6 +10,7 @@ import type { IFrameCloseOptions, IFrameMessage } from 'proton-pass-extension/ap import { IFrameMessageType } from 'proton-pass-extension/app/content/types'; import { c } from 'ttag'; +import { UpsellRef } from '@proton/pass/constants'; import { useNavigateToUpgrade } from '@proton/pass/hooks/useNavigateToUpgrade'; import { contentScriptMessage, sendMessage } from '@proton/pass/lib/extension/message'; import { createTelemetryEvent } from '@proton/pass/lib/telemetry/event'; @@ -30,7 +31,7 @@ type Props = { export const ItemsList: VFC = ({ hostname, items, needsUpgrade, visible, onMessage, onClose }) => { const { settings } = useIFrameContext(); - const navigateToUpgrade = useNavigateToUpgrade(); + const navigateToUpgrade = useNavigateToUpgrade({ upsellRef: UpsellRef.LIMIT_AUTOFILL }); useEffect(() => { if (visible) { diff --git a/applications/pass-extension/src/app/pages/settings/Settings.tsx b/applications/pass-extension/src/app/pages/settings/Settings.tsx index 97f608bf69f..27326b22e34 100644 --- a/applications/pass-extension/src/app/pages/settings/Settings.tsx +++ b/applications/pass-extension/src/app/pages/settings/Settings.tsx @@ -16,6 +16,7 @@ import { Avatar } from '@proton/atoms/Avatar'; import { Icon, Tabs, useNotifications } from '@proton/components'; import { UpgradeButton } from '@proton/pass/components/Layout/Button/UpgradeButton'; import { LockConfirmContextProvider } from '@proton/pass/components/Lock/LockConfirmContextProvider'; +import { UpsellRef } from '@proton/pass/constants'; import { pageMessage } from '@proton/pass/lib/extension/message'; import { selectPassPlan, @@ -127,7 +128,7 @@ const SettingsTabs: FC<{ pathname: string }> = ({ pathname }) => { )})`} - + )} diff --git a/applications/pass-extension/src/app/popup/Views/Header/MenuDropdown.tsx b/applications/pass-extension/src/app/popup/Views/Header/MenuDropdown.tsx index 245616b0129..e6b55cab778 100644 --- a/applications/pass-extension/src/app/popup/Views/Header/MenuDropdown.tsx +++ b/applications/pass-extension/src/app/popup/Views/Header/MenuDropdown.tsx @@ -28,6 +28,7 @@ import { Submenu } from '@proton/pass/components/Menu/Submenu'; import { VaultMenu } from '@proton/pass/components/Menu/Vault/VaultMenu'; import { useVaultActions } from '@proton/pass/components/Vault/VaultActionsProvider'; import { VaultIcon } from '@proton/pass/components/Vault/VaultIcon'; +import { UpsellRef } from '@proton/pass/constants'; import { useMenuItems } from '@proton/pass/hooks/useMenuItems'; import { selectHasRegisteredLock, @@ -133,7 +134,7 @@ export const MenuDropdown: VFC = () => { {passPlan !== UserPassPlan.PLUS && (
- +
)} diff --git a/applications/pass-extension/src/app/popup/Views/Sidebar/ItemsListPlaceholder.tsx b/applications/pass-extension/src/app/popup/Views/Sidebar/ItemsListPlaceholder.tsx index b0a347a359e..6a12525eb9d 100644 --- a/applications/pass-extension/src/app/popup/Views/Sidebar/ItemsListPlaceholder.tsx +++ b/applications/pass-extension/src/app/popup/Views/Sidebar/ItemsListPlaceholder.tsx @@ -14,6 +14,7 @@ import { UpgradeButton } from '@proton/pass/components/Layout/Button/UpgradeButt import { Card } from '@proton/pass/components/Layout/Card/Card'; import { itemTypeToIconName } from '@proton/pass/components/Layout/Icon/ItemIcon'; import { SubTheme } from '@proton/pass/components/Layout/Theme/types'; +import { UpsellRef } from '@proton/pass/constants'; import { isWritableVault } from '@proton/pass/lib/vaults/vault.predicates'; import { selectAllVaults, selectOwnReadOnlyVaults, selectShare, selectVaultLimits } from '@proton/pass/store/selectors'; import type { ItemType } from '@proton/pass/types'; @@ -95,7 +96,7 @@ export const ItemsListPlaceholder: VFC = () => { {c('Info') .t`You have exceeded the number of vaults included in your subscription. New items can only be created in your first two vaults. To create new items in all vaults upgrade your subscription.`} - + ); } diff --git a/applications/pass-extension/src/lib/hooks/useOnboardingListener.ts b/applications/pass-extension/src/lib/hooks/useOnboardingListener.ts index 49eea75b10e..3198c009645 100644 --- a/applications/pass-extension/src/lib/hooks/useOnboardingListener.ts +++ b/applications/pass-extension/src/lib/hooks/useOnboardingListener.ts @@ -2,8 +2,10 @@ import { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useSpotlight } from '@proton/pass/components/Spotlight/SpotlightProvider'; +import { UpsellRef } from '@proton/pass/constants'; import { useOnboardingMessages } from '@proton/pass/hooks/useOnboardingMessages'; import { popupMessage, sendMessage } from '@proton/pass/lib/extension/message'; +import { isEOY } from '@proton/pass/lib/onboarding/upselling'; import { selectCreatedItemsCount } from '@proton/pass/store/selectors'; import type { WorkerMessageWithSender } from '@proton/pass/types'; import { OnboardingMessage, WorkerMessageType } from '@proton/pass/types'; @@ -36,7 +38,13 @@ export const useOnboardingListener = () => { async ({ message }) => { await wait(200); if (message === OnboardingMessage.PENDING_SHARE_ACCESS) setPendingShareAccess(true); - if (message === OnboardingMessage.EARLY_ACCESS) setUpselling('early-access'); + if (message === OnboardingMessage.EARLY_ACCESS) { + setUpselling({ + type: 'early-access', + upsellRef: isEOY() ? UpsellRef.EOY_2023 : UpsellRef.EARLY_ACCESS, + }); + } + setOnboardingMessage(message ? definitions[message] ?? null : null); } ); diff --git a/packages/pass/components/Form/Field/Control/ExtraFieldsControl.tsx b/packages/pass/components/Form/Field/Control/ExtraFieldsControl.tsx index 0b2f541e044..9285ecf5b7f 100644 --- a/packages/pass/components/Form/Field/Control/ExtraFieldsControl.tsx +++ b/packages/pass/components/Form/Field/Control/ExtraFieldsControl.tsx @@ -1,6 +1,7 @@ import { type VFC, useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { UpsellRef } from '@proton/pass/constants'; import { selectExtraFieldLimits } from '@proton/pass/store/selectors'; import type { UnsafeItemExtraField } from '@proton/pass/types'; import { isEmptyString } from '@proton/pass/utils/string/is-empty-string'; @@ -27,7 +28,9 @@ export const ExtraFieldsControl: VFC = ({ extraFields, const key = `${index}-${fieldName}`; if (needsUpgrade) { - return ; + return ( + + ); } switch (type) { diff --git a/packages/pass/components/Form/Field/Control/UpgradeControl.tsx b/packages/pass/components/Form/Field/Control/UpgradeControl.tsx index 1ec9bf5c55b..9a469a9755a 100644 --- a/packages/pass/components/Form/Field/Control/UpgradeControl.tsx +++ b/packages/pass/components/Form/Field/Control/UpgradeControl.tsx @@ -2,16 +2,18 @@ import { type VFC } from 'react'; import { type IconName } from '@proton/components/components'; import { UpgradeButton } from '@proton/pass/components/Layout/Button/UpgradeButton'; +import { type UpsellRef } from '@proton/pass/constants'; import { ValueControl } from './ValueControl'; type UpgradeControlProps = { icon: IconName; label: string; + upsellRef: UpsellRef; }; -export const UpgradeControl: VFC = ({ icon, label }) => ( +export const UpgradeControl: VFC = ({ icon, label, upsellRef }) => ( - + ); diff --git a/packages/pass/components/Import/ImportVaultsPickerModal.tsx b/packages/pass/components/Import/ImportVaultsPickerModal.tsx index b37d44b26cb..9a41f26c68a 100644 --- a/packages/pass/components/Import/ImportVaultsPickerModal.tsx +++ b/packages/pass/components/Import/ImportVaultsPickerModal.tsx @@ -9,6 +9,7 @@ import { Button } from '@proton/atoms/Button'; import { Card } from '@proton/atoms/Card'; import type { ModalProps } from '@proton/components/components'; import { ModalTwo, ModalTwoContent, ModalTwoFooter, ModalTwoHeader } from '@proton/components/components'; +import { UpsellRef } from '@proton/pass/constants'; import { type ImportPayload, type ImportVault } from '@proton/pass/lib/import/types'; import { selectDefaultVault, @@ -96,7 +97,7 @@ export const ImportVaultsPickerModal: VFC = ({ payload, <> {c('Warning') .t`Your subscription does not allow you to create multiple vaults. All items will be imported to your first vault. To import into multiple vaults upgrade your subscription.`} - + ) : ( c('Warning').t`You cannot create more vaults than your subscription allows.` diff --git a/packages/pass/components/Invite/VaultAccessManager.tsx b/packages/pass/components/Invite/VaultAccessManager.tsx index 1093ee60262..b3f0cf3a6f4 100644 --- a/packages/pass/components/Invite/VaultAccessManager.tsx +++ b/packages/pass/components/Invite/VaultAccessManager.tsx @@ -14,6 +14,7 @@ import { PanelHeader } from '@proton/pass/components/Layout/Panel/PanelHeader'; import { ShareMember } from '@proton/pass/components/Share/ShareMember'; import { PendingExistingMember, PendingNewMember } from '@proton/pass/components/Share/SharePendingMember'; import { SharedVaultItem } from '@proton/pass/components/Vault/SharedVaultItem'; +import { UpsellRef } from '@proton/pass/constants'; import { useShareAccessOptionsPolling } from '@proton/pass/hooks/useShareAccessOptionsPolling'; import { isShareManageable } from '@proton/pass/lib/shares/share.predicates'; import { isVaultMemberLimitReached } from '@proton/pass/lib/vaults/vault.predicates'; @@ -60,7 +61,13 @@ export const VaultAccessManager: FC = ({ shareId }) => { const warning = (() => { if (canManage && memberLimitReached) { - const upgradeLink = ; + const upgradeLink = ( + + ); return plan === UserPassPlan.FREE ? c('Warning').jt`You have reached the limit of users in this vault. ${upgradeLink}` : c('Warning').t`You have reached the limit of members who can access this vault.`; diff --git a/packages/pass/components/Item/Alias/Alias.modal.tsx b/packages/pass/components/Item/Alias/Alias.modal.tsx index be0a884dcd5..b7b1a3e5598 100644 --- a/packages/pass/components/Item/Alias/Alias.modal.tsx +++ b/packages/pass/components/Item/Alias/Alias.modal.tsx @@ -13,6 +13,7 @@ import { Card } from '@proton/pass/components/Layout/Card/Card'; import { SidebarModal } from '@proton/pass/components/Layout/Modal/SidebarModal'; import { Panel } from '@proton/pass/components/Layout/Panel/Panel'; import { PanelHeader } from '@proton/pass/components/Layout/Panel/PanelHeader'; +import { UpsellRef } from '@proton/pass/constants'; import type { SanitizedAliasOptions } from '@proton/pass/hooks/useAliasOptions'; import { validateAliasForm } from '@proton/pass/lib/validation/alias'; import { selectAliasLimits } from '@proton/pass/store/selectors'; @@ -82,7 +83,7 @@ export const AliasModal = ({ /* if user has reached his alias limit prompt * him to upgrade his plan*/ needsUpgrade ? ( - + ) : ( ) : ( - + ), ]} /> diff --git a/packages/pass/components/Vault/VaultSelect.tsx b/packages/pass/components/Vault/VaultSelect.tsx index b9883560dfc..c45aeaed5a6 100644 --- a/packages/pass/components/Vault/VaultSelect.tsx +++ b/packages/pass/components/Vault/VaultSelect.tsx @@ -14,6 +14,7 @@ import { SidebarModal } from '@proton/pass/components/Layout/Modal/SidebarModal' import { Panel } from '@proton/pass/components/Layout/Panel/Panel'; import { PanelHeader } from '@proton/pass/components/Layout/Panel/PanelHeader'; import { VaultIcon } from '@proton/pass/components/Vault/VaultIcon'; +import { UpsellRef } from '@proton/pass/constants'; import type { VaultShareItem, WithItemCount } from '@proton/pass/store/reducers'; import { selectVaultLimits } from '@proton/pass/store/selectors'; import { NOOP_LIST_SELECTOR } from '@proton/pass/store/selectors/utils'; @@ -63,7 +64,9 @@ export const VaultSelect: VFC = ({ downgradeMessage, onSubmit, optionsSel > , - ...(didDowngrade ? [] : []), + ...(didDowngrade + ? [] + : []), ]} /> } diff --git a/packages/pass/constants.ts b/packages/pass/constants.ts index f89a84953ad..18f93001f57 100644 --- a/packages/pass/constants.ts +++ b/packages/pass/constants.ts @@ -32,3 +32,26 @@ export const PASS_BF_2023_DATES = [ export const PASS_EOY_DATE_END = +new Date('2024-01-03T09:00:00'); export const PASS_UPGRADE_PATH = 'pass/upgrade'; export const PASS_EOY_PATH = `pass/signup?plan=pass2023&cycle=12&coupon=EOY2023`; + +export enum UpsellRefPrefix { + Extension = 'pass_extension', + Web = 'pass_web', + Desktop = 'pass_desktop', +} + +export enum UpsellRef { + DEFAULT = 'banner', + EARLY_ACCESS = 'early_access_banner', + FREE_TRIAL = 'free_trial_banner', + EOY_2023 = 'eoy_2023_banner', + LIMIT_2FA = '2fa_limit', + LIMIT_ALIAS = 'alias_limit', + LIMIT_AUTOFILL = 'autofill_limit', + LIMIT_CC = 'credit_card_limit', + LIMIT_EXTRA_FIELD = 'extra_field_limit', + LIMIT_IMPORT = 'import_limit', + LIMIT_SHARING = 'limit_share', + LIMIT_VAULT = 'vault_limit', + MENU = 'menu', + SETTING = 'setting', +} diff --git a/packages/pass/hooks/useNavigateToUpgrade.ts b/packages/pass/hooks/useNavigateToUpgrade.ts index cb38632b492..5ca243b7db7 100644 --- a/packages/pass/hooks/useNavigateToUpgrade.ts +++ b/packages/pass/hooks/useNavigateToUpgrade.ts @@ -1,6 +1,14 @@ import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider'; +import { getUpgradePath } from '@proton/pass/lib/onboarding/upselling'; -export const useNavigateToUpgrade = () => { - const { onLink, config } = usePassCore(); - return () => onLink(`${config.SSO_URL}/pass/upgrade`); +import { type UpsellRef, UpsellRefPrefix } from '../constants'; + +/** `pathRef` will be passed to the upgrade link */ +export const useNavigateToUpgrade = (options: { path?: string; upsellRef: UpsellRef }) => { + const { onLink, config, endpoint } = usePassCore(); + + const refPrefix = UpsellRefPrefix[endpoint === 'web' ? 'Web' : 'Extension']; + const refSearch = options?.upsellRef ? `&ref=${refPrefix}_${options.upsellRef}` : ''; + + return () => onLink(`${config.SSO_URL}/${options.path ?? getUpgradePath()}${refSearch}`, { replace: true }); }; diff --git a/packages/pass/hooks/useOnboardingMessages.ts b/packages/pass/hooks/useOnboardingMessages.ts index ab8c0e08c41..96499acc6b3 100644 --- a/packages/pass/hooks/useOnboardingMessages.ts +++ b/packages/pass/hooks/useOnboardingMessages.ts @@ -8,7 +8,7 @@ import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider'; import type { SpotlightMessageDefinition } from '@proton/pass/components/Spotlight/SpotlightContent'; import { FiveStarIcon, ShieldIcon } from '@proton/pass/components/Spotlight/SpotlightIcon'; import { useSpotlight } from '@proton/pass/components/Spotlight/SpotlightProvider'; -import { PASS_BF_MONTHLY_PRICE, PASS_LEARN_MORE_URL } from '@proton/pass/constants'; +import { PASS_BF_MONTHLY_PRICE, PASS_LEARN_MORE_URL, UpsellRef } from '@proton/pass/constants'; import { usePassConfig } from '@proton/pass/hooks/usePassConfig'; import { selectUser } from '@proton/pass/store/selectors'; import { OnboardingMessage } from '@proton/pass/types'; @@ -51,11 +51,17 @@ export const useOnboardingMessages = () => { message: c('Info') .t`7 days to try premium features for free. Only during your first week of ${BRAND_NAME}.`, className: 'ui-orange', - onClose: () => acknowledge(OnboardingMessage.TRIAL), + onClose: () => acknowledge(OnboardingMessage.TRIAL, () => setUpselling(null)), action: { label: c('Label').t`Learn more`, type: 'link', - onClick: () => acknowledge(OnboardingMessage.TRIAL, () => setUpselling('free-trial')), + onClick: () => + acknowledge(OnboardingMessage.TRIAL, () => + setUpselling({ + type: 'free-trial', + upsellRef: UpsellRef.FREE_TRIAL, + }) + ), }, }, [OnboardingMessage.SECURE_EXTENSION]: { diff --git a/packages/pass/lib/onboarding/rules.ts b/packages/pass/lib/onboarding/rules.ts index d9a7d98d90c..ab98969bf35 100644 --- a/packages/pass/lib/onboarding/rules.ts +++ b/packages/pass/lib/onboarding/rules.ts @@ -19,7 +19,6 @@ import { UNIX_DAY } from '@proton/pass/utils/time/constants'; import { getEpoch } from '@proton/pass/utils/time/get-epoch'; import { createOnboardingRule } from './service'; -import { isEOY } from './utils'; export const createPendingShareAccessRule = (store: Store) => createOnboardingRule({ @@ -110,6 +109,6 @@ export const createEarlyAccessRule = (store: Store) => message: OnboardingMessage.EARLY_ACCESS, when: (previous) => { const passPlan = selectPassPlan(store.getState()); - return !previous && passPlan !== UserPassPlan.PLUS && isEOY(); + return !previous && passPlan !== UserPassPlan.PLUS; }, }); diff --git a/packages/pass/lib/onboarding/upselling.ts b/packages/pass/lib/onboarding/upselling.ts new file mode 100644 index 00000000000..5374bee6dab --- /dev/null +++ b/packages/pass/lib/onboarding/upselling.ts @@ -0,0 +1,4 @@ +import { PASS_EOY_DATE_END, PASS_EOY_PATH, PASS_UPGRADE_PATH } from '@proton/pass/constants'; + +export const isEOY = () => +new Date() < PASS_EOY_DATE_END; +export const getUpgradePath = () => (isEOY() ? PASS_EOY_PATH : PASS_UPGRADE_PATH); diff --git a/packages/pass/lib/onboarding/utils.ts b/packages/pass/lib/onboarding/utils.ts deleted file mode 100644 index 3ae84d1d90f..00000000000 --- a/packages/pass/lib/onboarding/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PASS_EOY_DATE_END } from '@proton/pass/constants'; - -export const isEOY = () => +new Date() < PASS_EOY_DATE_END;