Premium Promo: Add missing sections & bugfixes (#4321)

This commit is contained in:
Alexander Zinchuk 2024-03-01 14:02:56 -05:00
parent aea5598e55
commit f85fe40c7e
23 changed files with 215 additions and 174 deletions

View File

@ -2,7 +2,7 @@
import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiLimitType } from '../../../global/types';
import type { ApiLimitType, ApiPremiumSection } from '../../../global/types';
import type { ApiAppConfig } from '../../types';
import {
@ -107,7 +107,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
maxUniqueReactions: appConfig.reactions_uniq_max,
premiumBotUsername: appConfig.premium_bot_username,
premiumInvoiceSlug: appConfig.premium_invoice_slug,
premiumPromoOrder: appConfig.premium_promo_order,
premiumPromoOrder: appConfig.premium_promo_order as ApiPremiumSection[],
isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked,
defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id,
topicsPinnedLimit: appConfig.topics_pinned_limit,

View File

@ -1,5 +1,6 @@
import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiPremiumSection } from '../../../global/types';
import type {
ApiBoostsStatus,
ApiCheckedGiftCode,
@ -181,7 +182,7 @@ export function buildApiPremiumPromo(promo: GramJs.help.PremiumPromo): ApiPremiu
return {
statusText,
statusEntities: statusEntities.map(buildApiMessageEntity),
videoSections,
videoSections: videoSections as ApiPremiumSection[],
videos: videos.map(buildApiDocument).filter(Boolean),
options: periodOptions.map(buildApiPremiumSubscriptionOption),
};

View File

@ -1,4 +1,4 @@
import type { ApiLimitType, CallbackAction } from '../../global/types';
import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types';
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
import type { ApiUser } from './users';
@ -185,7 +185,7 @@ export interface ApiAppConfig {
premiumInvoiceSlug: string;
premiumBotUsername: string;
isPremiumPurchaseBlocked: boolean;
premiumPromoOrder: string[];
premiumPromoOrder: ApiPremiumSection[];
defaultEmojiStatusesStickerSetId: string;
maxUniqueReactions: number;
topicsPinnedLimit: number;

View File

@ -1,3 +1,4 @@
import type { ApiPremiumSection } from '../../global/types';
import type { ApiInvoiceContainer } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages';
@ -64,7 +65,7 @@ export interface ApiReceipt {
}
export interface ApiPremiumPromo {
videoSections: string[];
videoSections: ApiPremiumSection[];
videos: ApiDocument[];
statusText: string;
statusEntities: ApiMessageEntity[];

View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.0383 19.7139C21.7257 18.9733 18.8699 16.9701 18.0434 16.8556C17.5312 16.7846 16.415 18.2067 16.1413 19.8246C15.9733 20.8174 16.2209 21.81 16.8961 22.4129C18.6717 23.9983 22.351 20.4546 22.0383 19.7139ZM26.3572 7.22842C24.5742 6.62499 20.3579 8.74054 19.1894 14.9306C19.1894 15.2314 21.6203 16.9738 23.2952 17.5755C24.1503 17.8827 27.2917 14.6979 27.9631 11.5094C28.363 9.60974 27.2686 7.53689 26.3572 7.22842ZM11.6516 21.8097C11.1613 21.3904 7.50032 20.3232 6.79835 20.6606C6.09639 20.998 5.8308 25.3063 8.4321 25.8826C11.0334 26.4588 12.142 22.229 11.6516 21.8097ZM7.3738 18.2653C7.8641 18.6845 11.6074 19.9798 12.3094 19.6424C13.0114 19.3049 17.4904 10.042 12.9081 9.07884C11.4585 8.77415 8.62607 10.6618 7.77039 13.1382C6.93613 15.5526 7.13175 18.0583 7.3738 18.2653Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 943 B

View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 13.4V23.1373C6 24.3489 6 24.9547 6.23959 25.2352C6.44749 25.4786 6.75934 25.6078 7.07846 25.5827C7.44624 25.5538 7.87462 25.1254 8.73137 24.2686L10.0627 22.9373C10.4086 22.5914 10.5816 22.4184 10.7834 22.2947C10.9624 22.1851 11.1575 22.1043 11.3615 22.0553C11.5917 22 11.8363 22 12.3255 22H19.6C21.8402 22 22.9603 22 23.816 21.564C24.5686 21.1805 25.1805 20.5686 25.564 19.816C26 18.9603 26 17.8402 26 15.6V13.4C26 11.1598 26 10.0397 25.564 9.18404C25.1805 8.43139 24.5686 7.81947 23.816 7.43597C22.9603 7 21.8402 7 19.6 7H12.4C10.1598 7 9.03969 7 8.18404 7.43597C7.43139 7.81947 6.81947 8.43139 6.43597 9.18404C6 10.0397 6 11.1598 6 13.4ZM17.335 12.0025C17.335 11.2646 16.7363 10.6666 15.9983 10.6675C15.2617 10.6685 14.665 11.2659 14.665 12.0025V13.0003C14.7696 13 14.881 13 15 13H17C17.119 13 17.2304 13 17.335 13.0003V12.0025ZM13.335 12.0025V13.1152C13.3006 13.1264 13.2672 13.1387 13.2346 13.1522C12.7446 13.3552 12.3552 13.7446 12.1522 14.2346C12 14.6022 12 15.0681 12 16C12 16.9319 12 17.3978 12.1522 17.7654C12.3552 18.2554 12.7446 18.6448 13.2346 18.8478C13.6022 19 14.0681 19 15 19H17C17.9319 19 18.3978 19 18.7654 18.8478C19.2554 18.6448 19.6448 18.2554 19.8478 17.7654C20 17.3978 20 16.9319 20 16C20 15.0681 20 14.6022 19.8478 14.2346C19.6448 13.7446 19.2554 13.3552 18.7654 13.1522C18.7328 13.1387 18.6994 13.1264 18.665 13.1152V12.0025C18.665 10.5294 17.4698 9.33567 15.9966 9.33753C14.5261 9.33938 13.335 10.532 13.335 12.0025Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -10,6 +10,7 @@ import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import Button from '../ui/Button';
import Modal, { ANIMATION_DURATION } from '../ui/Modal';
@ -36,33 +37,36 @@ const ReadDateModal = ({ isOpen, user }: OwnProps & StateProps) => {
} = getActions();
const userName = getUserFirstOrLastName(user);
const handleShowReadTime = () => {
const handleShowReadTime = useLastCallback(() => {
updateGlobalPrivacySettings({ shouldHideReadMarks: false });
closeGetReadDateModal();
setTimeout(() => {
showNotification({ message: lang('PremiumReadSet') });
}, CLOSE_ANIMATION_DURATION);
};
});
const handleOpenPremium = () => {
const handleOpenPremium = useLastCallback(() => {
closeGetReadDateModal();
setTimeout(() => {
openPremiumModal();
}, CLOSE_ANIMATION_DURATION);
};
});
const handleClose = useLastCallback(() => {
closeGetReadDateModal();
});
return (
<Modal isSlim isOpen={isOpen} onClose={closeGetReadDateModal}>
<Modal isSlim isOpen={isOpen} onClose={handleClose}>
<div className={styles.container} dir={lang.isRtl ? 'rtl' : undefined}>
<Button
className={styles.closeButton}
color="translucent"
round
size="smaller"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closeGetReadDateModal()}
onClick={handleClose}
ariaLabel="Close"
>
<Icon name="close" />
@ -78,7 +82,6 @@ const ReadDateModal = ({ isOpen, user }: OwnProps & StateProps) => {
<p className={styles.desc}>{renderText(lang('PremiumReadText1', userName), ['simple_markdown'])}</p>
<Button
size="smaller"
// eslint-disable-next-line react/jsx-no-bind
onClick={handleShowReadTime}
className={styles.button}
>
@ -87,7 +90,6 @@ const ReadDateModal = ({ isOpen, user }: OwnProps & StateProps) => {
<Separator className={styles.separator}>{lang('PremiumOr')}</Separator>
<h2 className={styles.header}>{lang('PremiumReadHeader2')}</h2>
<p className={styles.desc}>{renderText(lang('PremiumReadText2', userName), ['simple_markdown'])}</p>
{/* eslint-disable-next-line react/jsx-no-bind */}
<Button withPremiumGradient size="smaller" onClick={handleOpenPremium} className={styles.button}>
{lang('PremiumLastSeenButton2')}
</Button>

View File

@ -66,7 +66,7 @@ const SliderDots: FC<OwnProps> = ({
styles.dot,
index === active && styles.active,
(isPreLast || isPreFirst) && styles.medium,
(isLast || isFirst) && styles.small,
(isLast || isFirst || isInvisible) && styles.small,
isInvisible && styles.invisible,
)}
/>

View File

@ -129,7 +129,6 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
)}
<ListItem
// eslint-disable-next-line react/jsx-no-bind
onClick={handleDownloadLog}
icon="bug"
>

View File

@ -33,6 +33,12 @@ const SettingsPrivacyLastSeen = ({
(isEnabled) => updateGlobalPrivacySettings({ shouldHideReadMarks: isEnabled }),
);
const handleOpenPremiumModal = useLastCallback(() => {
openPremiumModal({
initialSection: 'last_seen',
});
});
return (
<>
{canShowHideReadTime && (
@ -50,8 +56,7 @@ const SettingsPrivacyLastSeen = ({
<div className="settings-item">
<ListItem
leftElement={<PremiumIcon className="icon" withGradient big />}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumModal()}
onClick={handleOpenPremiumModal}
>
{isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')}
</ListItem>

View File

@ -358,7 +358,6 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
className="settings-folders-list-item mb-0"
icon="link"
multiline
// eslint-disable-next-line react/jsx-no-bind
onClick={handleEditInviteClick}
clickArg={invite.url}
>

View File

@ -591,7 +591,7 @@ const Main: FC<OwnProps & StateProps> = ({
<AttachBotInstallModal bot={attachBotToInstall} />
<AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} />
<MessageListHistoryHandler />
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
<PremiumMainModal isOpen={isPremiumModalOpen} />
<PremiumLimitReachedModal limit={limitReached} />
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />

View File

@ -41,8 +41,6 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
isOpen,
user,
gifts,
monthlyCurrency,
monthlyAmount,
}) => {
const { openPremiumModal, closeGiftPremiumModal, openUrl } = getActions();
@ -56,14 +54,12 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
return undefined;
}
const cheaperGift = renderedGifts.reduce((acc, gift) => {
return gift.amount < firstGift?.amount ? gift : firstGift;
const basicGift = renderedGifts.reduce((acc, gift) => {
return gift.months < firstGift.months ? gift : firstGift;
}, firstGift);
return cheaperGift.currency === monthlyCurrency && monthlyAmount
? monthlyAmount
: Math.floor(cheaperGift.amount / cheaperGift.months);
}, [firstGift, renderedGifts, monthlyAmount, monthlyCurrency]);
return Math.floor(basicGift.amount / basicGift.months);
}, [firstGift, renderedGifts]);
useEffect(() => {
if (isOpen) {
@ -165,14 +161,12 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const { forUserId, monthlyCurrency, monthlyAmount } = selectTabState(global).giftPremiumModal || {};
const { forUserId } = selectTabState(global).giftPremiumModal || {};
const user = forUserId ? selectUser(global, forUserId) : undefined;
const gifts = user ? selectUserFullInfo(global, user.id)?.premiumGifts : undefined;
return {
user,
gifts,
monthlyCurrency,
monthlyAmount: monthlyAmount ? Number(monthlyAmount) : undefined,
};
})(GiftPremiumModal));

View File

@ -1,22 +1,24 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import { hexToRgb, lerpRgb } from '../../../util/switchTheme';
import renderText from '../../common/helpers/renderText';
import useLastCallback from '../../../hooks/useLastCallback';
import ListItem from '../../ui/ListItem';
import styles from './PremiumFeatureItem.module.scss';
type OwnProps = {
type OwnProps<T> = {
icon: string;
isFontIcon?: boolean;
title: string;
text: string;
index: number;
count: number;
onClick?: VoidFunction;
section: T;
onClick?: (section: T) => void;
};
const COLORS = [
@ -24,22 +26,28 @@ const COLORS = [
'#9873FF', '#768DFF', '#55A5FC', '#52B0C9', '#4FBC93', '#4CC663',
].map(hexToRgb);
const PremiumFeatureItem: FC<OwnProps> = ({
// eslint-disable-next-line @typescript-eslint/comma-dangle
const PremiumFeatureItem = <T,>({
icon,
isFontIcon,
title,
text,
index,
count,
section,
onClick,
}) => {
}: OwnProps<T>) => {
const newIndex = (index / count) * COLORS.length;
const colorA = COLORS[Math.floor(newIndex)];
const colorB = COLORS[Math.ceil(newIndex)] ?? colorA;
const { r, g, b } = lerpRgb(colorA, colorB, 0.5);
const handleClick = useLastCallback(() => {
onClick?.(section);
});
return (
<ListItem buttonClassName={styles.root} onClick={onClick} inactive={!onClick}>
<ListItem buttonClassName={styles.root} onClick={handleClick} inactive={!onClick}>
{isFontIcon ? (
<i
className={buildClassName(styles.fontIcon, `icon icon-${icon}`)}

View File

@ -1,11 +1,14 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useMemo, useRef, useState,
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { toggleExtraClass } from '../../../lib/teact/teact-dom';
import type { ApiPremiumPromo } from '../../../api/types';
import type { ApiLimitType, GlobalState } from '../../../global/types';
import type { ApiPremiumPromo, ApiPremiumSubscriptionOption } from '../../../api/types';
import type { ApiLimitTypeForPromo, ApiPremiumSection, GlobalState } from '../../../global/types';
import { PREMIUM_BOTTOM_VIDEOS, PREMIUM_FEATURE_SECTIONS, PREMIUM_LIMITS_ORDER } from '../../../config';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
@ -13,6 +16,7 @@ import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import usePrevious from '../../../hooks/usePrevious';
import SliderDots from '../../common/SliderDots';
@ -24,7 +28,7 @@ import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo';
import styles from './PremiumFeatureModal.module.scss';
export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
export const PREMIUM_FEATURE_TITLES: Record<ApiPremiumSection, string> = {
double_limits: 'PremiumPreviewLimits',
infinite_reactions: 'PremiumPreviewReactions2',
premium_stickers: 'PremiumPreviewStickers',
@ -40,9 +44,11 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
translations: 'PremiumPreviewTranslations',
stories: 'PremiumPreviewStories',
saved_tags: 'PremiumPreviewTags2',
last_seen: 'PremiumPreviewLastSeen',
message_privacy: 'PremiumPreviewMessagePrivacy',
};
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
double_limits: 'PremiumPreviewLimitsDescription',
infinite_reactions: 'PremiumPreviewReactions2Description',
premium_stickers: 'PremiumPreviewStickersDescription',
@ -58,56 +64,11 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
translations: 'PremiumPreviewTranslationsDescription',
stories: 'PremiumPreviewStoriesDescription',
saved_tags: 'PremiumPreviewTagsDescription2',
last_seen: 'PremiumPreviewLastSeenDescription',
message_privacy: 'PremiumPreviewMessagePrivacyDescription',
};
export const PREMIUM_FEATURE_SECTIONS = [
'stories',
'double_limits',
'more_upload',
'faster_download',
'voice_to_text',
'no_ads',
'infinite_reactions',
'premium_stickers',
'animated_emoji',
'advanced_chat_management',
'profile_badge',
'animated_userpics',
'emoji_status',
'translations',
'saved_tags',
];
const PREMIUM_BOTTOM_VIDEOS: string[] = [
'faster_download',
'voice_to_text',
'advanced_chat_management',
'infinite_reactions',
'profile_badge',
'animated_userpics',
'emoji_status',
'translations',
'saved_tags',
];
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType,
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned'
>;
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
'channels',
'dialogFolderPinned',
'channelsPublic',
'savedGifs',
'stickersFaved',
'aboutLength',
'captionLength',
'dialogFilters',
'dialogFiltersChats',
'recommendedChannels',
];
const LIMITS_TITLES: Record<ApiLimitTypeWithoutUpload, string> = {
const LIMITS_TITLES: Record<ApiLimitTypeForPromo, string> = {
channels: 'GroupsAndChannelsLimitTitle',
dialogFolderPinned: 'PinChatsLimitTitle',
channelsPublic: 'PublicLinksLimitTitle',
@ -120,7 +81,7 @@ const LIMITS_TITLES: Record<ApiLimitTypeWithoutUpload, string> = {
recommendedChannels: 'SimilarChannelsLimitTitle',
};
const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeWithoutUpload, string> = {
const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeForPromo, string> = {
channels: 'GroupsAndChannelsLimitSubtitle',
dialogFolderPinned: 'PinChatsLimitSubtitle',
channelsPublic: 'PublicLinksLimitSubtitle',
@ -136,11 +97,12 @@ const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeWithoutUpload, string> = {
const BORDER_THRESHOLD = 20;
type OwnProps = {
initialSection: string;
initialSection: ApiPremiumSection;
promo: ApiPremiumPromo;
isPremium?: boolean;
limits?: NonNullable<GlobalState['appConfig']>['limits'];
premiumPromoOrder?: string[];
premiumPromoOrder?: ApiPremiumSection[];
subscriptionOption?: ApiPremiumSubscriptionOption;
onBack: VoidFunction;
onClickSubscribe: (startParam?: string) => void;
};
@ -151,6 +113,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
isPremium,
limits,
premiumPromoOrder,
subscriptionOption,
onBack,
onClickSubscribe,
}) => {
@ -171,27 +134,42 @@ const PremiumFeatureModal: FC<OwnProps> = ({
return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section));
}, [premiumPromoOrder]);
function handleClick() {
const subscriptionButtonText = useMemo(() => {
if (!subscriptionOption) return undefined;
const { amount, months, currency } = subscriptionOption;
const perMonthPrice = Math.floor(amount / months);
return isPremium ? lang('OK') : lang('SubscribeToPremium', formatCurrency(perMonthPrice, currency, lang.code));
}, [isPremium, lang, subscriptionOption]);
const handleClick = useLastCallback(() => {
onClickSubscribe(initialSection);
}
});
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { clientWidth, scrollLeft: scrollLeftOriginal } = e.currentTarget;
const target = e.currentTarget;
const { clientWidth, scrollLeft: scrollLeftOriginal } = target;
const scrollLeft = Math.round(scrollLeftOriginal);
const left = scrollLeft % (clientWidth);
const progress = left / (clientWidth);
e.currentTarget.style.setProperty('--scroll-progress', progress.toString());
e.currentTarget.style.setProperty('--abs-scroll-progress', Math.abs(progress).toString());
const reverseIndex = Math.ceil((scrollLeft + 1) / clientWidth);
setReverseAnimationSlideIndex(reverseIndex);
const prevElement = e.currentTarget.querySelector(`#premium_feature_preview_video_${reverseIndex - 1}`);
const reverseElement = e.currentTarget.querySelector(`#premium_feature_preview_video_${reverseIndex}`);
prevElement?.classList.toggle('reverse', false);
reverseElement?.classList.toggle('reverse', true);
const prevElement = target.querySelector<HTMLDivElement>(`#premium_feature_preview_video_${reverseIndex - 1}`);
const reverseElement = target.querySelector<HTMLDivElement>(`#premium_feature_preview_video_${reverseIndex}`);
requestMutation(() => {
target.style.setProperty('--scroll-progress', progress.toString());
target.style.setProperty('--abs-scroll-progress', Math.abs(progress).toString());
if (prevElement) toggleExtraClass(prevElement, 'reverse', false);
if (reverseElement) toggleExtraClass(reverseElement, 'reverse', true);
});
if (isScrolling) return;
const slide = Math.round(scrollLeft / clientWidth);
@ -215,7 +193,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
.then(stopScrolling);
}, [currentSlideIndex, filteredSections, initialSection, prevInitialSection]);
const handleSelectSlide = useCallback(async (index: number) => {
const handleSelectSlide = useLastCallback(async (index: number) => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
@ -224,10 +202,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
startScrolling();
await animateHorizontalScroll(scrollContainer, scrollContainer.clientWidth * index, 800);
stopScrolling();
}, []);
// TODO Support all subscription options
const month = promo.options.find((option) => option.months === 1)!;
});
return (
<div className={styles.root}>
@ -254,7 +229,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
{lang(PREMIUM_FEATURE_TITLES.double_limits)}
</h2>
<div className={buildClassName(styles.limitsContent, 'custom-scroll')} onScroll={handleLimitsScroll}>
{LIMITS_ORDER.map((limit, i) => {
{PREMIUM_LIMITS_ORDER.map((limit, i) => {
const defaultLimit = limits?.[limit][0].toString();
const premiumLimit = limits?.[limit][1].toString();
return (
@ -263,7 +238,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
description={lang(LIMITS_DESCRIPTIONS[limit], premiumLimit)}
leftValue={defaultLimit}
rightValue={premiumLimit}
colorStepProgress={i / (LIMITS_ORDER.length - 1)}
colorStepProgress={i / (PREMIUM_LIMITS_ORDER.length - 1)}
/>
);
})}
@ -333,16 +308,16 @@ const PremiumFeatureModal: FC<OwnProps> = ({
active={currentSlideIndex}
onSelectSlide={handleSelectSlide}
/>
<Button
className={buildClassName(styles.button)}
isShiny={!isPremium}
withPremiumGradient={!isPremium}
onClick={isPremium ? onBack : handleClick}
>
{isPremium
? lang('OK')
: lang('SubscribeToPremium', formatCurrency(Number(month.amount), month.currency, lang.code))}
</Button>
{subscriptionButtonText && (
<Button
className={buildClassName(styles.button)}
isShiny={!isPremium}
withPremiumGradient={!isPremium}
onClick={isPremium ? onBack : handleClick}
>
{subscriptionButtonText}
</Button>
)}
</div>
</div>
);

View File

@ -1,15 +1,15 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useMemo, useRef, useState,
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiPremiumPromo, ApiPremiumSubscriptionOption, ApiSticker, ApiStickerSet, ApiUser,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { ApiPremiumSection, GlobalState } from '../../../global/types';
import { TME_LINK_PREFIX } from '../../../config';
import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
import {
selectIsCurrentUserPremium, selectStickerSet,
@ -33,7 +33,6 @@ import Transition from '../../ui/Transition';
import PremiumFeatureItem from './PremiumFeatureItem';
import PremiumFeatureModal, {
PREMIUM_FEATURE_DESCRIPTIONS,
PREMIUM_FEATURE_SECTIONS,
PREMIUM_FEATURE_TITLES,
} from './PremiumFeatureModal';
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
@ -45,8 +44,10 @@ import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
import PremiumFile from '../../../assets/premium/PremiumFile.svg';
import PremiumLastSeen from '../../../assets/premium/PremiumLastSeen.svg';
import PremiumLimits from '../../../assets/premium/PremiumLimits.svg';
import PremiumLogo from '../../../assets/premium/PremiumLogo.svg';
import PremiumMessagePrivacy from '../../../assets/premium/PremiumMessagePrivacy.svg';
import PremiumReactions from '../../../assets/premium/PremiumReactions.svg';
import PremiumSpeed from '../../../assets/premium/PremiumSpeed.svg';
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
@ -59,7 +60,7 @@ import PremiumVoice from '../../../assets/premium/PremiumVoice.svg';
const LIMIT_ACCOUNTS = 4;
const STATUS_EMOJI_SIZE = 8 * REM;
const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
stories: PremiumStatus,
double_limits: PremiumLimits,
infinite_reactions: PremiumReactions,
@ -75,6 +76,8 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
emoji_status: PremiumStatus,
translations: PremiumTranslate,
saved_tags: PremiumTags,
last_seen: PremiumLastSeen,
message_privacy: PremiumMessagePrivacy,
};
export type OwnProps = {
@ -84,12 +87,11 @@ export type OwnProps = {
type StateProps = {
currentUserId?: string;
promo?: ApiPremiumPromo;
isClosing?: boolean;
fromUser?: ApiUser;
fromUserStatusEmoji?: ApiSticker;
fromUserStatusSet?: ApiStickerSet;
toUser?: ApiUser;
initialSection?: string;
initialSection?: ApiPremiumSection;
isPremium?: boolean;
isSuccess?: boolean;
isGift?: boolean;
@ -101,7 +103,7 @@ type StateProps = {
limits?: NonNullable<GlobalState['appConfig']>['limits'];
premiumSlug?: string;
premiumBotUsername?: string;
premiumPromoOrder?: string[];
premiumPromoOrder?: ApiPremiumSection[];
};
const PremiumMainModal: FC<OwnProps & StateProps> = ({
@ -120,7 +122,6 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
limits,
premiumSlug,
premiumBotUsername,
isClosing,
isSuccess,
isGift,
toUser,
@ -135,14 +136,23 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
const lang = useLang();
const [isHeaderHidden, setHeaderHidden] = useState(true);
const [currentSection, setCurrentSection] = useState<string | undefined>(initialSection);
const [currentSection, setCurrentSection] = useState<ApiPremiumSection | undefined>(initialSection);
const [selectedSubscriptionOption, setSubscriptionOption] = useState<ApiPremiumSubscriptionOption>();
const handleOpen = useCallback((section: string | undefined) => {
return () => {
setCurrentSection(section);
};
}, []);
useEffect(() => {
if (!isOpen) {
setHeaderHidden(true);
setCurrentSection(undefined);
}
}, [isOpen]);
const handleOpenSection = useLastCallback((section: ApiPremiumSection) => {
setCurrentSection(section);
});
const handleResetSection = useLastCallback(() => {
setCurrentSection(undefined);
});
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop } = e.currentTarget;
@ -166,20 +176,20 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
}
});
const handleClick = useCallback(() => {
const handleClick = useLastCallback(() => {
if (selectedSubscriptionOption) {
handleClickWithStartParam(String(selectedSubscriptionOption.months));
} else {
handleClickWithStartParam();
}
}, [selectedSubscriptionOption, handleClickWithStartParam]);
});
const handleChangeSubscriptionOption = useCallback((months: number) => {
const handleChangeSubscriptionOption = useLastCallback((months: number) => {
const foundOption = promo?.options.find((option) => option.months === months);
setSubscriptionOption(foundOption);
}, [promo]);
});
const showConfetti = useCallback(() => {
const showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
@ -193,7 +203,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
height,
});
}
}, [isOpen, requestConfetti]);
});
useEffect(() => {
if (isSuccess) {
@ -336,10 +346,8 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
return (
<Modal
className={styles.root}
// eslint-disable-next-line react/jsx-no-bind
onCloseAnimationEnd={() => closePremiumModal({ isClosed: true })}
onClose={closePremiumModal}
isOpen={isOpen && !isClosing}
isOpen={isOpen}
dialogRef={dialogRef}
>
<Transition name="slide" activeKey={currentSection ? 1 : 0} className={styles.transition}>
@ -392,7 +400,8 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
index={index}
count={filteredSections.length}
onClick={handleOpen(section)}
section={section}
onClick={handleOpenSection}
/>
);
})}
@ -420,13 +429,13 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
) : (
<PremiumFeatureModal
initialSection={currentSection}
onBack={handleOpen(undefined)}
onBack={handleResetSection}
promo={promo}
// eslint-disable-next-line react/jsx-no-bind
onClickSubscribe={handleClickWithStartParam}
isPremium={isPremium}
limits={limits}
premiumPromoOrder={premiumPromoOrder}
subscriptionOption={selectedSubscriptionOption}
/>
)}
</Transition>
@ -448,7 +457,6 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
currentUserId: global.currentUserId,
promo: premiumModal?.promo,
isClosing: premiumModal?.isClosing,
isSuccess: premiumModal?.isSuccess,
isGift: premiumModal?.isGift,
monthsAmount: premiumModal?.monthsAmount,

View File

@ -108,6 +108,7 @@ const PremiumFeaturePreviewVideo = ({
isFontIcon
index={index}
count={STORY_FEATURE_ORDER.length}
section={section}
/>
);
})}

View File

@ -217,9 +217,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
setViewForumAsMessages({ chatId, isEnabled: true });
});
function handleRequestCall() {
const handleRequestCall = useLastCallback(() => {
requestMasterAndRequestCall({ userId: chatId });
}
});
const handleHotkeySearchClick = useLastCallback((e: KeyboardEvent) => {
if (!canSearch || !IS_APP || e.shiftKey) {
@ -384,7 +384,6 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
// eslint-disable-next-line react/jsx-no-bind
onClick={handleRequestCall}
ariaLabel="Call"
>

View File

@ -19,6 +19,7 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import useFlag from '../../../hooks/useFlag';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useTimeout from '../../../hooks/useTimeout';
@ -92,7 +93,7 @@ const SimilarChannels = ({
return undefined;
}, [similarChannels, shouldShowInChat, shoulRenderSkeleton]);
const handleToggle = () => {
const handleToggle = useLastCallback(() => {
toggleChannelRecommendations({ chatId });
if (shouldShowInChat) {
markNotShowing();
@ -101,7 +102,7 @@ const SimilarChannels = ({
markShowing();
markNotHiding();
}
};
});
return (
<div className={buildClassName(styles.root)}>
@ -144,7 +145,6 @@ const SimilarChannels = ({
<Button
className={styles.close}
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={handleToggle}
>
<Icon name="close" />

View File

@ -159,8 +159,7 @@ const StoryView = ({
styles.opacityFadeIn,
(storyView.isUserBlocked || storyView.areStoriesBlocked) && styles.blocked,
)}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleClick()}
onClick={handleClick}
rightElement={storyView.type === 'user' && storyView.reaction ? (
<ReactionStaticEmoji
reaction={storyView.reaction}

View File

@ -1,5 +1,5 @@
import type { ApiReactionEmoji } from './api/types';
import type { ApiLimitType } from './global/types';
import type { ApiLimitType, ApiLimitTypeForPromo, ApiPremiumSection } from './global/types';
export const APP_CODE_NAME = 'A';
export const APP_NAME = process.env.APP_NAME || `Telegram Web ${APP_CODE_NAME}`;
@ -347,3 +347,51 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
};
export const ONE_TIME_MEDIA_TTL_SECONDS = 2147483647;
// Premium
export const PREMIUM_FEATURE_SECTIONS = [
'stories',
'double_limits',
'more_upload',
'faster_download',
'voice_to_text',
'no_ads',
'infinite_reactions',
'premium_stickers',
'animated_emoji',
'advanced_chat_management',
'profile_badge',
'animated_userpics',
'emoji_status',
'translations',
'saved_tags',
'last_seen',
'message_privacy',
] as const;
export const PREMIUM_BOTTOM_VIDEOS: ApiPremiumSection[] = [
'faster_download',
'voice_to_text',
'advanced_chat_management',
'infinite_reactions',
'profile_badge',
'animated_userpics',
'emoji_status',
'translations',
'saved_tags',
'last_seen',
'message_privacy',
];
export const PREMIUM_LIMITS_ORDER: ApiLimitTypeForPromo[] = [
'channels',
'dialogFolderPinned',
'channelsPublic',
'savedGifs',
'stickersFaved',
'aboutLength',
'captionLength',
'dialogFilters',
'dialogFiltersChats',
'recommendedChannels',
];

View File

@ -363,15 +363,14 @@ addActionHandler('setPaymentStep', (global, actions, payload): ActionReturnType
});
addActionHandler('closePremiumModal', (global, actions, payload): ActionReturnType => {
const { isClosed, tabId = getCurrentTabId() } = payload || {};
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
if (!tabState.premiumModal) return undefined;
return updateTabState(global, {
premiumModal: {
...tabState.premiumModal,
...(isClosed && { isOpen: false }),
isClosing: !isClosed,
promo: tabState.premiumModal.promo, // Cache promo
isOpen: false,
},
}, tabId);
});
@ -415,15 +414,10 @@ addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promi
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
// TODO Support all subscription options
const month = result.promo.options.find((option) => option.months === 1)!;
global = updateTabState(global, {
giftPremiumModal: {
isOpen: true,
forUserId,
monthlyCurrency: month.currency,
monthlyAmount: String(month.amount),
},
}, tabId);
setGlobal(global);

View File

@ -75,6 +75,7 @@ import type {
ApiWebSession,
} from '../api/types';
import type { ApiCredentials } from '../components/payment/PaymentModal';
import type { PREMIUM_FEATURE_SECTIONS } from '../config';
import type { FoldersActions } from '../hooks/reducers/useFoldersReducer';
import type { ReducerAction } from '../hooks/useReducer';
import type { P2pMessage } from '../lib/secret-sauce';
@ -194,6 +195,12 @@ export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs' | 'recommendedChannels'
)>;
export type ApiLimitTypeForPromo = Exclude<ApiLimitType,
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned'
>;
export type ApiPremiumSection = typeof PREMIUM_FEATURE_SECTIONS[number];
export type TranslatedMessage = {
isPending?: boolean;
text?: ApiFormattedText;
@ -607,9 +614,8 @@ export type TabState = {
premiumModal?: {
isOpen?: boolean;
isClosing?: boolean;
promo: ApiPremiumPromo;
initialSection?: string;
initialSection?: ApiPremiumSection;
fromUserId?: string;
toUserId?: string;
isGift?: boolean;
@ -620,8 +626,6 @@ export type TabState = {
giftPremiumModal?: {
isOpen?: boolean;
forUserId?: string;
monthlyCurrency?: string;
monthlyAmount?: string;
};
limitReachedModal?: {
@ -2877,16 +2881,14 @@ export interface ActionPayloads {
// Premium
openPremiumModal: ({
initialSection?: string;
initialSection?: ApiPremiumSection;
fromUserId?: string;
toUserId?: string;
isSuccess?: boolean;
isGift?: boolean;
monthsAmount?: number;
} & WithTabId) | undefined;
closePremiumModal: ({
isClosed?: boolean;
} & WithTabId) | undefined;
closePremiumModal: WithTabId | undefined;
transcribeAudio: {
chatId: string;