Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 128 additions & 20 deletions ts/components/dialog/SessionProInfoModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ import { 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';
import { Localizer } from '../basic/Localizer';

export enum SessionProInfoVariant {
MESSAGE_CHARACTER_LIMIT = 0,
Expand All @@ -39,6 +44,8 @@ export enum SessionProInfoVariant {
ALREADY_PRO_PROFILE_PICTURE_ANIMATED = 4,
GENERIC = 5,
GROUP_ACTIVATED = 6,
ONE_TIME_EXPIRE_SOON = 7,
ONE_TIME_EXPIRED = 8,
}

const StyledContentContainer = styled.div`
Expand Down Expand Up @@ -69,21 +76,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 (
<StyledAnimatedCTAImageContainer>
<StyledAnimatedCTAImageContainer noColor={noColor}>
<StyledCTAImage src={ctaLayerSrc} />
<StyledAnimationImage src={animatedLayerSrc} style={animationStyle} />
</StyledAnimatedCTAImageContainer>
Expand Down Expand Up @@ -153,6 +163,13 @@ function getFeatureList(variant: SessionProInfoVariant) {
return ['proFeatureListLongerMessages', 'proFeatureListLargerGroups'] as const;
case SessionProInfoVariant.GENERIC: // yes generic has the same as above, reversed...
return ['proFeatureListLargerGroups', 'proFeatureListLongerMessages'] as const;
case SessionProInfoVariant.ONE_TIME_EXPIRE_SOON:
case SessionProInfoVariant.ONE_TIME_EXPIRED:
return [
'proFeatureListPinnedConversations',
'proFeatureListPinnedConversations',
'proFeatureListAnimatedDisplayPicture',
] as const;
case SessionProInfoVariant.GROUP_ACTIVATED:
return [];
default:
Expand All @@ -161,14 +178,43 @@ function getFeatureList(variant: SessionProInfoVariant) {
}
}

function getDescription(variant: SessionProInfoVariant): ReactNode {
function ProExpiringSoonDescription() {
const { data } = useProAccessDetails();
return <Localizer token="proExpiringSoonDescription" time={data.expiryTimeRelativeString} />;
}

function getDescription(variant: SessionProInfoVariant, userHasProExpired: boolean): ReactNode {
switch (variant) {
case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT:
return tr('proCallToActionPinnedConversationsMoreThan');
return (
<Localizer
token={
userHasProExpired
? 'proRenewPinFiveConversations'
: 'proCallToActionPinnedConversationsMoreThan'
}
/>
);
case SessionProInfoVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED:
return tr('proCallToActionPinnedConversations');
return (
<Localizer
token={
userHasProExpired
? 'proRenewPinMoreConversations'
: 'proCallToActionPinnedConversations'
}
/>
);
case SessionProInfoVariant.PROFILE_PICTURE_ANIMATED:
return tr('proAnimatedDisplayPictureCallToActionDescription');
return (
<Localizer
token={
userHasProExpired
? 'proRenewAnimatedDisplayPicture'
: 'proAnimatedDisplayPictureCallToActionDescription'
}
/>
);
case SessionProInfoVariant.ALREADY_PRO_PROFILE_PICTURE_ANIMATED:
return (
<>
Expand All @@ -186,10 +232,22 @@ function getDescription(variant: SessionProInfoVariant): ReactNode {
);

case SessionProInfoVariant.MESSAGE_CHARACTER_LIMIT:
return tr('proCallToActionLongerMessages');
return (
<Localizer
token={userHasProExpired ? 'proRenewLongerMessages' : 'proCallToActionLongerMessages'}
/>
);

case SessionProInfoVariant.GENERIC:
return tr('proUserProfileModalCallToAction');
return (
<Localizer
token={userHasProExpired ? 'proRenewMaxPotential' : 'proUserProfileModalCallToAction'}
/>
);
case SessionProInfoVariant.ONE_TIME_EXPIRE_SOON:
return <ProExpiringSoonDescription />;
case SessionProInfoVariant.ONE_TIME_EXPIRED:
return <Localizer token="proExpiredDescription" />;
case SessionProInfoVariant.GROUP_ACTIVATED:
return (
<span>
Expand Down Expand Up @@ -223,12 +281,16 @@ function getImage(variant: SessionProInfoVariant): ReactNode {
return <StyledCTAImage src="images/cta_hero_char_limit.webp" />;
case SessionProInfoVariant.GROUP_ACTIVATED:
return <StyledCTAImage src="images/cta_hero_group_activated_admin.webp" />;

case SessionProInfoVariant.GENERIC:
case SessionProInfoVariant.ONE_TIME_EXPIRE_SOON:
case SessionProInfoVariant.ONE_TIME_EXPIRED:
return (
<AnimatedCTAImage
ctaLayerSrc="images/cta_hero_generic_base_layer.webp"
animatedLayerSrc="images/cta_hero_animated_profile_animation_layer.webp"
animationStyle={{ width: '8%', top: '59.2%', left: '85.5%' }}
noColor={variant === SessionProInfoVariant.ONE_TIME_EXPIRED}
/>
);

Expand All @@ -239,11 +301,11 @@ 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,
SessionProInfoVariant.ONE_TIME_EXPIRE_SOON,
].includes(variant);
}

Expand All @@ -261,6 +323,7 @@ export const proButtonProps = {
export function SessionProInfoModal(props: SessionProInfoState) {
const dispatch = useDispatch();
const hasPro = useCurrentUserHasPro();
const userHasExpiredPro = useCurrentUserHasExpiredPro();

function onClose() {
dispatch(updateSessionProInfoModal(null));
Expand All @@ -270,6 +333,9 @@ export function SessionProInfoModal(props: SessionProInfoState) {
return null;
}
const isGroupCta = props.variant === SessionProInfoVariant.GROUP_ACTIVATED;
const isOneTimeExpireModal =
props.variant === SessionProInfoVariant.ONE_TIME_EXPIRED ||
props.variant === SessionProInfoVariant.ONE_TIME_EXPIRE_SOON;

/**
* Note: the group activated cta is quite custom, but whatever the pro status of the current pro user,
Expand All @@ -296,14 +362,37 @@ export function SessionProInfoModal(props: SessionProInfoState) {
display: 'grid',
alignItems: 'center',
justifyItems: 'center',
gridTemplateColumns: !hasNoProAndNotGroupCta ? '1fr' : '1fr 1fr',
gridTemplateColumns: hasNoProAndNotGroupCta || isOneTimeExpireModal ? '1fr 1fr' : '1fr',
columnGap: 'var(--margins-sm)',
paddingInline: 'var(--margins-md)',
marginBottom: 'var(--margins-md)',
height: 'unset',
}}
>
{hasNoProAndNotGroupCta ? (
{isOneTimeExpireModal ? (
<SessionButtonShiny
{...proButtonProps}
shinyContainerStyle={{
width: '100%',
}}
buttonColor={SessionButtonColor.PrimaryDark}
onClick={() => {
onClose();
dispatch(
userSettingsModal({
userSettingsPage: 'proNonOriginating',
nonOriginatingVariant:
props.variant === SessionProInfoVariant.ONE_TIME_EXPIRED ? 'renew' : 'update',
hideBackButton: true,
centerAlign: true,
})
);
}}
dataTestId="modal-session-pro-confirm-button"
>
{tr(props.variant === SessionProInfoVariant.ONE_TIME_EXPIRED ? 'renew' : 'update')}
</SessionButtonShiny>
) : hasNoProAndNotGroupCta ? (
<SessionButtonShiny
{...proButtonProps}
shinyContainerStyle={{
Expand Down Expand Up @@ -332,7 +421,7 @@ export function SessionProInfoModal(props: SessionProInfoState) {
onClick={onClose}
dataTestId="modal-session-pro-cancel-button"
style={
!hasNoProAndNotGroupCta
!hasNoProAndNotGroupCta && !isOneTimeExpireModal
? { ...proButtonProps.style, width: '50%' }
: proButtonProps.style
}
Expand All @@ -344,22 +433,41 @@ export function SessionProInfoModal(props: SessionProInfoState) {
>
<SpacerSM />
<StyledCTATitle reverseDirection={!hasNoProAndNotGroupCta}>
{tr(isGroupCta ? 'proGroupActivated' : hasPro ? 'proActivated' : 'upgradeTo')}
<ProIconButton iconSize={'huge'} dataTestId="invalid-data-testid" onClick={undefined} />
{tr(
props.variant === SessionProInfoVariant.ONE_TIME_EXPIRED
? 'proExpired'
: props.variant === SessionProInfoVariant.ONE_TIME_EXPIRE_SOON
? 'proExpiringSoon'
: isGroupCta
? 'proGroupActivated'
: hasPro
? 'proActivated'
: userHasExpiredPro
? 'renew'
: 'upgradeTo'
)}
<ProIconButton
iconSize={'huge'}
dataTestId="invalid-data-testid"
onClick={undefined}
noColors={props.variant === SessionProInfoVariant.ONE_TIME_EXPIRED}
/>
</StyledCTATitle>
<SpacerXL />
<StyledContentContainer>
<StyledScrollDescriptionContainer>
{getDescription(props.variant)}
{getDescription(props.variant, userHasExpiredPro)}
</StyledScrollDescriptionContainer>
{hasNoProAndNotGroupCta ? (
{hasNoProAndNotGroupCta || isOneTimeExpireModal ? (
<StyledFeatureList>
{getFeatureList(props.variant).map(token => (
<FeatureListItem>{tr(token)}</FeatureListItem>
))}
<FeatureListItem customIconSrc={'images/sparkle-animated.svg'}>
{tr('proFeatureListLoadsMore')}
</FeatureListItem>
{!isOneTimeExpireModal ? (
<FeatureListItem customIconSrc={'images/sparkle-animated.svg'}>
{tr('proFeatureListLoadsMore')}
</FeatureListItem>
) : null}
</StyledFeatureList>
) : null}
</StyledContentContainer>
Expand Down
6 changes: 6 additions & 0 deletions ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export function ProPlaygroundPage() {
>
Animated Profile Picture (Has pro)
</SessionButton>
<SessionButton onClick={() => handleClick(SessionProInfoVariant.ONE_TIME_EXPIRE_SOON)}>
Expiring Soon
</SessionButton>
<SessionButton onClick={() => handleClick(SessionProInfoVariant.ONE_TIME_EXPIRED)}>
Expired
</SessionButton>
</Flex>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ export function ProNonOriginatingPage(modalState: {
userSettingsPage: 'proNonOriginating';
nonOriginatingVariant: ProNonOriginatingPageVariant;
overrideBackAction?: () => void;
hideBackButton?: boolean;
centerAlign?: boolean;
}) {
const backAction = useUserSettingsBackAction(modalState);
Expand All @@ -585,7 +586,11 @@ export function ProNonOriginatingPage(modalState: {
bigHeader={true}
showExitIcon={true}
floatingHeader={true}
extraLeftButton={backOnClick ? <ModalBackButton onClick={backOnClick} /> : undefined}
extraLeftButton={
backOnClick && !modalState.hideBackButton ? (
<ModalBackButton onClick={backOnClick} />
) : undefined
}
/>
}
onClose={closeAction || undefined}
Expand Down
1 change: 1 addition & 0 deletions ts/state/ducks/modalDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type WithUserSettingsPage =
userSettingsPage: 'proNonOriginating';
nonOriginatingVariant: ProNonOriginatingPageVariant;
overrideBackAction?: () => void;
hideBackButton?: boolean;
centerAlign?: boolean;
};

Expand Down
Loading