diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx index b7b78082c..07dff3480 100644 --- a/ts/components/conversation/composition/CompositionBox.tsx +++ b/ts/components/conversation/composition/CompositionBox.tsx @@ -61,7 +61,7 @@ import { useShowBlockUnblock } from '../../menuAndSettingsHooks/useShowBlockUnbl import { showLocalizedPopupDialog } from '../../dialog/LocalizedPopupDialog'; import { formatNumber } from '../../../util/i18n/formatting/generics'; import { getFeatureFlag } from '../../../state/ducks/types/releasedFeaturesReduxTypes'; -import { SessionProInfoVariant, showSessionProInfoDialog } from '../../dialog/SessionProInfoModal'; +import { ProCTAVariant, showSessionProInfoDialog } from '../../dialog/SessionProInfoModal'; import { tStripped } from '../../../localization/localeTools'; import type { ProcessedLinkPreviewThumbnailType } from '../../../webworker/workers/node/image_processor/image_processor'; import { selectWeAreProUser } from '../../../hooks/useParamSelector'; @@ -746,7 +746,7 @@ class CompositionBoxInner extends Component { const dispatch = window.inboxStore?.dispatch; if (dispatch) { if (isProAvailable && !hasPro) { - showSessionProInfoDialog(SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT, dispatch); + showSessionProInfoDialog(ProCTAVariant.MESSAGE_CHARACTER_LIMIT, dispatch); } else { showLocalizedPopupDialog( { diff --git a/ts/components/dialog/EditProfilePictureModal.tsx b/ts/components/dialog/EditProfilePictureModal.tsx index 02146e031..0d1ff6035 100644 --- a/ts/components/dialog/EditProfilePictureModal.tsx +++ b/ts/components/dialog/EditProfilePictureModal.tsx @@ -26,10 +26,7 @@ import { } from '../SessionWrapperModal'; import { useIsProAvailable } from '../../hooks/useIsProAvailable'; import { SpacerLG, SpacerSM } from '../basic/Text'; -import { - SessionProInfoVariant, - useShowSessionProInfoDialogCbWithVariant, -} from './SessionProInfoModal'; +import { ProCTAVariant, useShowSessionProInfoDialogCbWithVariant } from './SessionProInfoModal'; import { AvatarSize } from '../avatar/Avatar'; import { ProIconButton } from '../buttons/ProButton'; import { useProBadgeOnClickCb } from '../menuAndSettingsHooks/useProBadgeOnClickCb'; @@ -202,7 +199,7 @@ export const EditProfilePictureModal = ({ conversationId }: EditProfilePictureMo * All of those are taken care of as part of the `isProUser` check in the conversation model */ if (isProAvailable && !userHasPro && isNewAvatarAnimated && !isCommunity) { - handleShowProInfoModal(SessionProInfoVariant.PROFILE_PICTURE_ANIMATED); + handleShowProInfoModal(ProCTAVariant.ANIMATED_DISPLAY_PICTURE); window.log.debug('Attempted to upload an animated profile picture without pro!'); return; } diff --git a/ts/components/dialog/SessionProInfoModal.tsx b/ts/components/dialog/SessionProInfoModal.tsx index 784dd4633..9377e8b2c 100644 --- a/ts/components/dialog/SessionProInfoModal.tsx +++ b/ts/components/dialog/SessionProInfoModal.tsx @@ -1,5 +1,5 @@ import { isNil } from 'lodash'; -import { Dispatch, type ReactNode } from 'react'; +import { Dispatch, useMemo, type ReactNode } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import type { CSSProperties } from 'styled-components'; @@ -7,6 +7,7 @@ import { type SessionProInfoState, updateSessionProInfoModal, userSettingsModal, + UserSettingsModalState, } from '../../state/ducks/modalDialog'; import { SessionWrapperModal, @@ -23,22 +24,34 @@ import { import { SpacerSM, SpacerXL } from '../basic/Text'; import { LucideIcon } from '../icon/LucideIcon'; import { LUCIDE_ICONS_UNICODE } from '../icon/lucide'; -import { tr } from '../../localization/localeTools'; +import { MergedLocalizerTokens, tr } from '../../localization/localeTools'; import { FileIcon } from '../icon/FileIcon'; import { SessionButtonShiny } from '../basic/SessionButtonShiny'; import { useIsProAvailable } from '../../hooks/useIsProAvailable'; -import { useCurrentUserHasPro } from '../../hooks/useHasPro'; +import { + useCurrentUserHasExpiredPro, + useCurrentUserHasPro, + useProAccessDetails, +} from '../../hooks/useHasPro'; import { ProIconButton } from '../buttons/ProButton'; import { assertUnreachable } from '../../types/sqlSharedTypes'; - -export enum SessionProInfoVariant { - MESSAGE_CHARACTER_LIMIT = 0, - PINNED_CONVERSATION_LIMIT = 1, - PINNED_CONVERSATION_LIMIT_GRANDFATHERED = 2, - PROFILE_PICTURE_ANIMATED = 3, - ALREADY_PRO_PROFILE_PICTURE_ANIMATED = 4, - GENERIC = 5, - GROUP_ACTIVATED = 6, +import { Localizer } from '../basic/Localizer'; + +export enum ProCTAVariant { + GENERIC = 0, + // Feature - has expired sub variants + MESSAGE_CHARACTER_LIMIT = 1, + ANIMATED_DISPLAY_PICTURE = 2, + ANIMATED_DISPLAY_PICTURE_ACTIVATED = 3, + PINNED_CONVERSATION_LIMIT = 4, + PINNED_CONVERSATION_LIMIT_GRANDFATHERED = 5, + // Groups + GROUP_NON_ADMIN = 6, + GROUP_ADMIN = 7, + GROUP_ACTIVATED = 8, + // Special + EXPIRING_SOON = 9, + EXPIRED = 10, } const StyledContentContainer = styled.div` @@ -69,21 +82,24 @@ const StyledAnimationImage = styled.img` position: absolute; `; -const StyledAnimatedCTAImageContainer = styled.div` +const StyledAnimatedCTAImageContainer = styled.div<{ noColor?: boolean }>` position: relative; + ${props => (props.noColor ? 'filter: grayscale(100%);' : '')} `; function AnimatedCTAImage({ ctaLayerSrc, animatedLayerSrc, animationStyle, + noColor, }: { ctaLayerSrc: string; animatedLayerSrc: string; animationStyle: CSSProperties; + noColor?: boolean; }) { return ( - + @@ -141,35 +157,145 @@ function FeatureListItem({ ); } -function getFeatureList(variant: SessionProInfoVariant) { +function isVariantWithActionButton(variant: ProCTAVariant): boolean { + return ![ + ProCTAVariant.GROUP_NON_ADMIN, + ProCTAVariant.GROUP_ACTIVATED, + ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED, + ].includes(variant); +} + +// These CTAS have "Upgrade to" and "Renew" titles. +const variantsForNonGroupFeatures = [ + ProCTAVariant.MESSAGE_CHARACTER_LIMIT, + ProCTAVariant.ANIMATED_DISPLAY_PICTURE, + ProCTAVariant.PINNED_CONVERSATION_LIMIT, + ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED, + ProCTAVariant.GENERIC, +] as const; + +type VariantForNonGroupFeature = (typeof variantsForNonGroupFeatures)[number]; + +function isFeatureVariant(variant: ProCTAVariant): variant is VariantForNonGroupFeature { + return variantsForNonGroupFeatures.includes(variant as any); +} + +const variantsWithoutFeatureList = [ + ProCTAVariant.GROUP_NON_ADMIN, + ProCTAVariant.GROUP_ACTIVATED, + ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED, +] as const; + +type VariantWithoutFeatureList = (typeof variantsWithoutFeatureList)[number]; +type VariantWithFeatureList = Exclude; + +function isProFeatureListCTA(variant: ProCTAVariant): variant is VariantWithFeatureList { + return !variantsWithoutFeatureList.includes(variant as any); +} + +enum ProFeatureKey { + LONGER_MESSAGES = 'proFeatureListLongerMessages', + MORE_PINNED_CONVOS = 'proFeatureListPinnedConversations', + ANIMATED_DP = 'proFeatureListAnimatedDisplayPicture', + LARGER_GROUPS = 'proFeatureListLargerGroups', +} + +function getBaseFeatureList(variant: VariantWithFeatureList) { switch (variant) { - case SessionProInfoVariant.PROFILE_PICTURE_ANIMATED: - return ['proFeatureListAnimatedDisplayPicture', 'proFeatureListLargerGroups'] as const; - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT: - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: - return ['proFeatureListPinnedConversations', 'proFeatureListLargerGroups'] as const; - case SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT: - case SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED: - return ['proFeatureListLongerMessages', 'proFeatureListLargerGroups'] as const; - case SessionProInfoVariant.GENERIC: // yes generic has the same as above, reversed... - return ['proFeatureListLargerGroups', 'proFeatureListLongerMessages'] as const; - case SessionProInfoVariant.GROUP_ACTIVATED: - return []; + case ProCTAVariant.MESSAGE_CHARACTER_LIMIT: + return [ProFeatureKey.LONGER_MESSAGES, ProFeatureKey.MORE_PINNED_CONVOS]; + + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE: + return [ProFeatureKey.ANIMATED_DP, ProFeatureKey.LONGER_MESSAGES]; + + case ProCTAVariant.PINNED_CONVERSATION_LIMIT: + case ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: + return [ProFeatureKey.MORE_PINNED_CONVOS, ProFeatureKey.LONGER_MESSAGES]; + + case ProCTAVariant.GENERIC: // yes generic has the same as above, reversed... + return [ProFeatureKey.LONGER_MESSAGES, ProFeatureKey.MORE_PINNED_CONVOS]; + + case ProCTAVariant.EXPIRING_SOON: + case ProCTAVariant.EXPIRED: + return [ + ProFeatureKey.LONGER_MESSAGES, + ProFeatureKey.MORE_PINNED_CONVOS, + ProFeatureKey.ANIMATED_DP, + ]; + + case ProCTAVariant.GROUP_ADMIN: + return [ProFeatureKey.LARGER_GROUPS, ProFeatureKey.LONGER_MESSAGES]; + default: assertUnreachable(variant, 'getFeatureList unreachable case'); throw new Error('unreachable'); } } -function getDescription(variant: SessionProInfoVariant): ReactNode { +function FeatureList({ variant }: { variant: ProCTAVariant }) { + const featureList = useMemo(() => { + if (!isProFeatureListCTA(variant)) { + return []; + } + const features = getBaseFeatureList(variant).map(token => ( + {tr(token)} + )); + + // Expiry related CTAs dont show the "more" feature item + if (variant !== ProCTAVariant.EXPIRED && variant !== ProCTAVariant.EXPIRING_SOON) { + features.push( + + {tr('proFeatureListLoadsMore')} + + ); + } + return features; + }, [variant]); + + return featureList.length ? {featureList} : null; +} + +function ProExpiringSoonDescription() { + const { data } = useProAccessDetails(); + return ; +} + +function getDescription(variant: ProCTAVariant, userHasProExpired: boolean): ReactNode { switch (variant) { - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT: - return tr('proCallToActionPinnedConversationsMoreThan'); - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: - return tr('proCallToActionPinnedConversations'); - case SessionProInfoVariant.PROFILE_PICTURE_ANIMATED: - return tr('proAnimatedDisplayPictureCallToActionDescription'); - case SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED: + case ProCTAVariant.PINNED_CONVERSATION_LIMIT: + return ( + + ); + + case ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: + return ( + + ); + + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE: + return ( + + ); + + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED: return ( <> @@ -185,32 +311,51 @@ function getDescription(variant: SessionProInfoVariant): ReactNode { ); - case SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT: - return tr('proCallToActionLongerMessages'); + case ProCTAVariant.MESSAGE_CHARACTER_LIMIT: + return ( + + ); + + case ProCTAVariant.GENERIC: + return ( + + ); + + case ProCTAVariant.EXPIRING_SOON: + return ; - case SessionProInfoVariant.GENERIC: - return tr('proUserProfileModalCallToAction'); - case SessionProInfoVariant.GROUP_ACTIVATED: + case ProCTAVariant.EXPIRED: + return ; + + // TODO: Group CTA string dont all exist yet and need to be implemented later + case ProCTAVariant.GROUP_ADMIN: + case ProCTAVariant.GROUP_NON_ADMIN: + case ProCTAVariant.GROUP_ACTIVATED: return ( {tr('proGroupActivatedDescription')}{' '} ); + default: assertUnreachable(variant, 'getDescription unreachable case'); throw new Error('unreachable'); } } -function getImage(variant: SessionProInfoVariant): ReactNode { +function getImage(variant: ProCTAVariant): ReactNode { switch (variant) { - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT: - case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: + case ProCTAVariant.PINNED_CONVERSATION_LIMIT: + case ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED: return ; - case SessionProInfoVariant.PROFILE_PICTURE_ANIMATED: - case SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED: + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE: + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED: return ( ); - case SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT: + case ProCTAVariant.MESSAGE_CHARACTER_LIMIT: return ; - case SessionProInfoVariant.GROUP_ACTIVATED: + + // TODO: Group CTA images dont exist yet and need to be implemented later + case ProCTAVariant.GROUP_ADMIN: + case ProCTAVariant.GROUP_NON_ADMIN: + case ProCTAVariant.GROUP_ACTIVATED: return ; - case SessionProInfoVariant.GENERIC: + + case ProCTAVariant.GENERIC: + case ProCTAVariant.EXPIRING_SOON: + case ProCTAVariant.EXPIRED: return ( ); @@ -238,13 +391,60 @@ function getImage(variant: SessionProInfoVariant): ReactNode { } } -function isProVisibleCTA(variant: SessionProInfoVariant): boolean { - // This is simple now but if we ever add multiple this needs to become a list - return [ - SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED, - SessionProInfoVariant.GENERIC, - SessionProInfoVariant.GROUP_ACTIVATED, - ].includes(variant); +function CtaTitle({ variant }: { variant: ProCTAVariant }) { + const userHasExpiredPro = useCurrentUserHasExpiredPro(); + + const titleText = useMemo(() => { + if (isFeatureVariant(variant)) { + return ; + } + + switch (variant) { + // TODO: Group CTA titles arent finalised and need to be implemneted later + case ProCTAVariant.GROUP_NON_ADMIN: + return ; + + case ProCTAVariant.GROUP_ADMIN: + return ; + + case ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED: + return ; + + case ProCTAVariant.GROUP_ACTIVATED: + return ; + + case ProCTAVariant.EXPIRING_SOON: + return ; + + case ProCTAVariant.EXPIRED: + return ; + + default: + assertUnreachable(variant, 'CtaTitle'); + throw new Error('unreachable'); + } + }, [variant, userHasExpiredPro]); + + const isTitleDirectionReversed = useMemo(() => { + return [ + ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED, + ProCTAVariant.GROUP_ACTIVATED, + ProCTAVariant.EXPIRING_SOON, + ProCTAVariant.EXPIRED, + ].includes(variant); + }, [variant]); + + return ( + + {titleText} + + + ); } // TODO: we might want to make this a specific button preset. As its used for all pro/sesh stuff @@ -258,25 +458,113 @@ export const proButtonProps = { }, } satisfies SessionButtonProps; +function Buttons({ + variant, + onClose, + afterActionButtonCallback, + actionButtonNextModalAfterCloseCallback, +}: { + variant: ProCTAVariant; + onClose: () => void; + afterActionButtonCallback?: () => void; + actionButtonNextModalAfterCloseCallback?: () => void; +}) { + const dispatch = useDispatch(); + + const actionButton = useMemo(() => { + if (!isVariantWithActionButton(variant)) { + return null; + } + + let settingsModalProps: UserSettingsModalState = { + userSettingsPage: 'pro', + hideBackButton: true, + hideHelp: true, + centerAlign: true, + afterCloseAction: actionButtonNextModalAfterCloseCallback, + }; + + let buttonTextKey: MergedLocalizerTokens = 'theContinue'; + + if (variant === ProCTAVariant.EXPIRED || variant === ProCTAVariant.EXPIRING_SOON) { + settingsModalProps = { + userSettingsPage: 'proNonOriginating', + nonOriginatingVariant: variant === ProCTAVariant.EXPIRED ? 'renew' : 'update', + hideBackButton: true, + centerAlign: true, + afterCloseAction: actionButtonNextModalAfterCloseCallback, + }; + + buttonTextKey = variant === ProCTAVariant.EXPIRED ? 'renew' : 'update'; + } + + return ( + { + onClose(); + dispatch(userSettingsModal(settingsModalProps)); + afterActionButtonCallback?.(); + }} + dataTestId="modal-session-pro-confirm-button" + > + {tr(buttonTextKey)} + + ); + }, [ + variant, + dispatch, + onClose, + actionButtonNextModalAfterCloseCallback, + afterActionButtonCallback, + ]); + + return ( + + {actionButton} + + {tr(actionButton && variant !== ProCTAVariant.EXPIRING_SOON ? 'cancel' : 'close')} + + + ); +} + export function SessionProInfoModal(props: SessionProInfoState) { const dispatch = useDispatch(); const hasPro = useCurrentUserHasPro(); + const userHasExpiredPro = useCurrentUserHasExpiredPro(); function onClose() { dispatch(updateSessionProInfoModal(null)); } - if (isNil(props?.variant) || (hasPro && !isProVisibleCTA(props.variant))) { + // NOTE: Feature CTAs shouldnt show for users with pro + if (isNil(props?.variant) || (hasPro && isFeatureVariant(props.variant))) { return null; } - const isGroupCta = props.variant === SessionProInfoVariant.GROUP_ACTIVATED; - - /** - * Note: the group activated cta is quite custom, but whatever the pro status of the current pro user, - * we do not want to show the CTA for "subscribe to pro". - * An admin have subscribed and that's all that's needed to make this group a Pro group. - */ - const hasNoProAndNotGroupCta = !hasPro && !isGroupCta; return ( - {hasNoProAndNotGroupCta ? ( - { - onClose(); - dispatch( - userSettingsModal({ - userSettingsPage: 'pro', - hideBackButton: true, - hideHelp: true, - centerAlign: true, - }) - ); - }} - dataTestId="modal-session-pro-confirm-button" - > - {tr('theContinue')} - - ) : null} - - {tr(!hasNoProAndNotGroupCta ? 'close' : 'cancel')} - - + } > - - {tr(isGroupCta ? 'proGroupActivated' : hasPro ? 'proActivated' : 'upgradeTo')} - - + - {getDescription(props.variant)} + {getDescription(props.variant, userHasExpiredPro)} - {hasNoProAndNotGroupCta ? ( - - {getFeatureList(props.variant).map(token => ( - {tr(token)} - ))} - - {tr('proFeatureListLoadsMore')} - - - ) : null} + ); } -export const showSessionProInfoDialog = ( - variant: SessionProInfoVariant, - dispatch: Dispatch -) => { +export const showSessionProInfoDialog = (variant: ProCTAVariant, dispatch: Dispatch) => { dispatch( updateSessionProInfoModal({ variant, @@ -379,7 +607,7 @@ export const showSessionProInfoDialog = ( ); }; -export const useShowSessionProInfoDialogCb = (variant: SessionProInfoVariant) => { +export const useShowSessionProInfoDialogCb = (variant: ProCTAVariant) => { const dispatch = useDispatch(); // TODO: remove once pro is released @@ -397,8 +625,8 @@ export const useShowSessionProInfoDialogCbWithVariant = () => { // TODO: remove once pro is released const isProAvailable = useIsProAvailable(); if (!isProAvailable) { - return (_: SessionProInfoVariant) => null; + return (_: ProCTAVariant) => null; } - return (variant: SessionProInfoVariant) => showSessionProInfoDialog(variant, dispatch); + return (variant: ProCTAVariant) => showSessionProInfoDialog(variant, dispatch); }; diff --git a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx index 07919a494..a1ab6dd27 100644 --- a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx +++ b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx @@ -3,10 +3,7 @@ import { FlagToggle } from '../FeatureFlags'; import { useFeatureFlag } from '../../../../state/ducks/types/releasedFeaturesReduxTypes'; import { SessionButton } from '../../../basic/SessionButton'; import { SpacerLG, SpacerXS } from '../../../basic/Text'; -import { - SessionProInfoVariant, - useShowSessionProInfoDialogCbWithVariant, -} from '../../SessionProInfoModal'; +import { ProCTAVariant, useShowSessionProInfoDialogCbWithVariant } from '../../SessionProInfoModal'; import { Flex } from '../../../basic/Flex'; import { LucideIcon } from '../../../icon/LucideIcon'; import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide'; @@ -54,28 +51,47 @@ export function ProPlaygroundPage() { - handleClick(SessionProInfoVariant.GENERIC)}> - Generic - - handleClick(SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT)}> +

Feature CTAs

+ Only visible to users without Pro + handleClick(ProCTAVariant.GENERIC)}>Generic + handleClick(ProCTAVariant.MESSAGE_CHARACTER_LIMIT)}> Character Count - handleClick(SessionProInfoVariant.PINNED_CONVERSATION_LIMIT)}> + handleClick(ProCTAVariant.PINNED_CONVERSATION_LIMIT)}> Pinned Conversations handleClick(SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED)} + onClick={() => handleClick(ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED)} > Pinned Conversations (Grandfathered) - handleClick(SessionProInfoVariant.PROFILE_PICTURE_ANIMATED)}> + handleClick(ProCTAVariant.ANIMATED_DISPLAY_PICTURE)}> Animated Profile Picture +

Pro Activated CTAs

+ Only visible to users with Pro handleClick(SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED)} + onClick={() => handleClick(ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED)} > Animated Profile Picture (Has pro) +

Pro Group CTAs

+ WIP + handleClick(ProCTAVariant.GROUP_ACTIVATED)}> + Group Activated + + handleClick(ProCTAVariant.GROUP_NON_ADMIN)}> + Group (Non-Admin) + + handleClick(ProCTAVariant.GROUP_ADMIN)}> + Group (Admin) + + +

Special CTAs

+ handleClick(ProCTAVariant.EXPIRING_SOON)}> + Expiring Soon + + handleClick(ProCTAVariant.EXPIRED)}>Expired
); diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx index 6686db268..0b8304e52 100644 --- a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx +++ b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx @@ -569,14 +569,12 @@ function ProPageButton({ variant }: VariantPageProps) { export function ProNonOriginatingPage(modalState: { userSettingsPage: 'proNonOriginating'; nonOriginatingVariant: ProNonOriginatingPageVariant; - overrideBackAction?: () => void; + hideBackButton?: boolean; centerAlign?: boolean; }) { const backAction = useUserSettingsBackAction(modalState); const closeAction = useUserSettingsCloseAction(modalState); - const backOnClick = modalState.overrideBackAction || backAction; - return ( : undefined} + extraLeftButton={ + backAction && !modalState.hideBackButton ? ( + + ) : undefined + } /> } onClose={closeAction || undefined} diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx index b3fdcaade..cca4f4448 100644 --- a/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx +++ b/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx @@ -48,9 +48,10 @@ import { SpacerMD } from '../../../../basic/Text'; import { sleepFor } from '../../../../../session/utils/Promise'; import { AnimatedSpinnerIcon } from '../../../../loading/spinner/AnimatedSpinnerIcon'; -// TODO: There are only 2 props here and both are passed to the nonorigin modal dispatch, can probably be in their own object +// TODO: All these props are passed to the nonorigin modal dispatch, they can probably be in their own object type SectionProps = { returnToThisModalAction: () => void; + afterCloseAction?: () => void; centerAlign?: boolean; }; @@ -145,7 +146,8 @@ export function ProHeroImage({ iconColor={noColors ? 'var(--disabled-color)' : 'var(--primary-color)'} iconSize={132} /> - + {/** We force LTR here as we always want the title to read "Session PRO" */} + full-brand-text ); @@ -960,6 +976,7 @@ export function ProSettingsPage(modalState: { hideBackButton?: boolean; hideHelp?: boolean; centerAlign?: boolean; + afterCloseAction?: () => void; }) { const dispatch = useDispatch(); const backAction = useUserSettingsBackAction(modalState); @@ -992,15 +1009,18 @@ export function ProSettingsPage(modalState: { {!modalState.hideHelp ? : null} diff --git a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx index 62d6a353b..44de6753c 100644 --- a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx +++ b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx @@ -77,7 +77,10 @@ export function useUserSettingsCloseAction(props: UserSettingsModalState) { case 'network': case 'pro': case 'proNonOriginating': - return () => dispatch(userSettingsModal(null)); + return () => { + dispatch(userSettingsModal(null)); + props.afterCloseAction?.(); + }; default: assertUnreachable(userSettingsPage, 'useCloseActionFromPage: invalid userSettingsPage'); @@ -87,6 +90,11 @@ export function useUserSettingsCloseAction(props: UserSettingsModalState) { export function useUserSettingsBackAction(modalState: UserSettingsModalState) { const dispatch = useDispatch(); + + if (modalState?.overrideBackAction) { + return modalState.overrideBackAction; + } + if (!modalState?.userSettingsPage || modalState?.userSettingsPage === 'default') { // no back button if we are on the default page return undefined; diff --git a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx index ca4259445..37ee0a40b 100644 --- a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx +++ b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx @@ -9,7 +9,7 @@ import { useIsPrivateAndFriend, } from '../../hooks/useParamSelector'; import { - SessionProInfoVariant, + ProCTAVariant, useShowSessionProInfoDialogCbWithVariant, } from '../dialog/SessionProInfoModal'; import { Constants } from '../../session'; @@ -66,7 +66,7 @@ export function useTogglePinConversationHandler(id: string) { return () => handleShowProDialog( pinnedConversationsCount > Constants.CONVERSATION.MAX_PINNED_CONVERSATIONS_STANDARD - ? SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED - : SessionProInfoVariant.PINNED_CONVERSATION_LIMIT + ? ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED + : ProCTAVariant.PINNED_CONVERSATION_LIMIT ); } diff --git a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx index f8acbc037..39c11138e 100644 --- a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx +++ b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx @@ -1,11 +1,22 @@ +import { useDispatch } from 'react-redux'; import { useIsProAvailable } from '../../hooks/useIsProAvailable'; import { ProMessageFeature } from '../../models/proMessageFeature'; +import { + updateConversationDetailsModal, + updateEditProfilePictureModal, + updateSessionProInfoModal, + userSettingsModal, +} from '../../state/ducks/modalDialog'; import { assertUnreachable } from '../../types/sqlSharedTypes'; import type { ContactNameContext } from '../conversation/ContactName/ContactNameContext'; import { - SessionProInfoVariant, + ProCTAVariant, useShowSessionProInfoDialogCbWithVariant, } from '../dialog/SessionProInfoModal'; +import { + useEditProfilePictureModal, + useUpdateConversationDetailsModal, +} from '../../state/selectors/modal'; type WithUserHasPro = { userHasPro: boolean }; type WithMessageSentWithProFeat = { messageSentWithProFeat: Array | null }; @@ -96,14 +107,14 @@ function isContactNameNoShowContext(context: ContactNameContext) { } } -function proFeatureToVariant(proFeature: ProMessageFeature): SessionProInfoVariant { +function proFeatureToVariant(proFeature: ProMessageFeature): ProCTAVariant { switch (proFeature) { case ProMessageFeature.PRO_INCREASED_MESSAGE_LENGTH: - return SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT; + return ProCTAVariant.MESSAGE_CHARACTER_LIMIT; case ProMessageFeature.PRO_ANIMATED_DISPLAY_PICTURE: - return SessionProInfoVariant.PROFILE_PICTURE_ANIMATED; + return ProCTAVariant.ANIMATED_DISPLAY_PICTURE; case ProMessageFeature.PRO_BADGE: - return SessionProInfoVariant.GENERIC; + return ProCTAVariant.GENERIC; default: assertUnreachable(proFeature, 'ProFeatureToVariant: unknown case'); throw new Error('unreachable'); @@ -120,9 +131,13 @@ function proFeatureToVariant(proFeature: ProMessageFeature): SessionProInfoVaria export function useProBadgeOnClickCb( opts: ProBadgeContext ): ShowTagWithCb | ShowTagNoCb | DoNotShowTag { + const dispatch = useDispatch(); const handleShowProInfoModal = useShowSessionProInfoDialogCbWithVariant(); const isProAvailable = useIsProAvailable(); + const editProfilePictureModal = useEditProfilePictureModal(); + const conversationDetailsModal = useUpdateConversationDetailsModal(); + const { context, args } = opts; if (!isProAvailable) { @@ -135,10 +150,21 @@ export function useProBadgeOnClickCb( return { show: true, cb: () => - handleShowProInfoModal( - args.userHasPro - ? SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED - : SessionProInfoVariant.PROFILE_PICTURE_ANIMATED + dispatch( + updateSessionProInfoModal({ + variant: args.userHasPro + ? ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED + : ProCTAVariant.ANIMATED_DISPLAY_PICTURE, + afterActionButtonCallback: () => { + dispatch(updateEditProfilePictureModal(null)); + dispatch(updateConversationDetailsModal(null)); + }, + actionButtonNextModalAfterCloseCallback: () => { + dispatch(userSettingsModal({ userSettingsPage: 'default' })); + dispatch(updateConversationDetailsModal(conversationDetailsModal)); + dispatch(updateEditProfilePictureModal(editProfilePictureModal)); + }, + }) ), }; } @@ -172,7 +198,7 @@ export function useProBadgeOnClickCb( cb: () => { handleShowProInfoModal( multiProFeatUsed - ? SessionProInfoVariant.GENERIC + ? ProCTAVariant.GENERIC : proFeatureToVariant(messageSentWithProFeat[0]) ); }, @@ -203,7 +229,7 @@ export function useProBadgeOnClickCb( // if this is a groupv2, the badge should open the "groupv2 activated" modal onclick return { show: true, - cb: () => handleShowProInfoModal(SessionProInfoVariant.GROUP_ACTIVATED), + cb: () => handleShowProInfoModal(ProCTAVariant.GROUP_ACTIVATED), }; } @@ -212,7 +238,7 @@ export function useProBadgeOnClickCb( return showNoCb; } // FOMO: user shown has pro but we don't: show CTA on click - return { show: true, cb: () => handleShowProInfoModal(SessionProInfoVariant.GENERIC) }; + return { show: true, cb: () => handleShowProInfoModal(ProCTAVariant.GENERIC) }; } if (context === 'character-count') { @@ -223,7 +249,7 @@ export function useProBadgeOnClickCb( // FOMO return { show: true, - cb: () => handleShowProInfoModal(SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT), + cb: () => handleShowProInfoModal(ProCTAVariant.MESSAGE_CHARACTER_LIMIT), }; } @@ -256,7 +282,7 @@ export function useProBadgeOnClickCb( return showNoCb; } - return { show: true, cb: () => handleShowProInfoModal(SessionProInfoVariant.GENERIC) }; + return { show: true, cb: () => handleShowProInfoModal(ProCTAVariant.GENERIC) }; } return showNoCb; } diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index 7d21295b9..ea79e065d 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -12,7 +12,7 @@ import type { ProNonOriginatingPageVariant, } from '../../types/ReduxTypes'; import { WithConvoId } from '../../session/types/with'; -import type { SessionProInfoVariant } from '../../components/dialog/SessionProInfoModal'; +import type { ProCTAVariant } from '../../components/dialog/SessionProInfoModal'; import type { TrArgs } from '../../localization/localeTools'; import { SessionButtonType } from '../../components/basic/SessionButton'; @@ -35,7 +35,10 @@ export type UserSettingsPage = | 'pro' | 'proNonOriginating'; -export type WithUserSettingsPage = +export type WithUserSettingsPage = { + overrideBackAction?: () => void; + afterCloseAction?: () => void; +} & ( | { userSettingsPage: Exclude } | { userSettingsPage: 'password'; @@ -50,9 +53,10 @@ export type WithUserSettingsPage = | { userSettingsPage: 'proNonOriginating'; nonOriginatingVariant: ProNonOriginatingPageVariant; - overrideBackAction?: () => void; + hideBackButton?: boolean; centerAlign?: boolean; - }; + } +); export type ConfirmModalState = SessionConfirmDialogProps | null; @@ -87,7 +91,15 @@ export type LocalizedPopupDialogState = { overrideButtons?: Array; } | null; -export type SessionProInfoState = { variant: SessionProInfoVariant } | null; +export type SessionProInfoState = { + variant: ProCTAVariant; + afterActionButtonCallback?: () => void; + // If the action button opens another modal, this callback is called after that next modal is closed. + // For example: If "ProInfoModal" is opened from the "EditProfilePictureModal", and "ProInfoModal"'s + // action button opens the "ProSettingsModal", we want to re-open "EditProfilePictureModal" + // when "ProSettingsModal" closes. + actionButtonNextModalAfterCloseCallback?: () => void; +} | null; export type UserProfileModalState = { /** this can be blinded or not */