TelegramPWA/src/components/main/premium/PremiumMainModal.tsx
2026-04-14 14:47:32 +02:00

542 lines
18 KiB
TypeScript

import type { FC } from '@teact';
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiPremiumPromo,
ApiPremiumSection,
ApiPremiumSubscriptionOption,
ApiStarGift,
ApiSticker,
ApiStickerSet,
ApiUser,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { LangPair } from '../../../types/language';
import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
import {
selectCustomEmoji,
selectIsCurrentUserPremium,
selectStickerSet,
selectTabState,
selectUser,
} from '../../../global/selectors';
import { selectPremiumLimit } from '../../../global/selectors/limits';
import buildClassName from '../../../util/buildClassName';
import { formatCountdownDays } from '../../../util/dates/oldDateFormat';
import { formatCurrency } from '../../../util/formatCurrency';
import { getStickerFromGift } from '../../common/helpers/gifts';
import { REM } from '../../common/helpers/mediaDimensions';
import renderText from '../../common/helpers/renderText';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useSyncEffect from '../../../hooks/useSyncEffect';
import CustomEmoji from '../../common/CustomEmoji';
import ParticlesHeader from '../../modals/common/ParticlesHeader.tsx';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import Transition from '../../ui/Transition';
import PremiumFeatureItem from './PremiumFeatureItem';
import PremiumFeatureModal, { PREMIUM_FEATURE_DESCRIPTIONS, PREMIUM_FEATURE_TITLES } from './PremiumFeatureModal';
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
import styles from './PremiumMainModal.module.scss';
import PremiumAds from '../../../assets/premium/PremiumAds.svg';
import PremiumAi from '../../../assets/premium/PremiumAi.svg';
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
import PremiumEffects from '../../../assets/premium/PremiumEffects.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 PremiumMessagePrivacy from '../../../assets/premium/PremiumMessagePrivacy.svg';
import PremiumNoforwards from '../../../assets/premium/PremiumNoForwardsPrivacy.svg';
import PremiumReactions from '../../../assets/premium/PremiumReactions.svg';
import PremiumSpeed from '../../../assets/premium/PremiumSpeed.svg';
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
import PremiumStickers from '../../../assets/premium/PremiumStickers.svg';
import PremiumTags from '../../../assets/premium/PremiumTags.svg';
import PremiumTranslate from '../../../assets/premium/PremiumTranslate.svg';
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
import PremiumVoice from '../../../assets/premium/PremiumVoice.svg';
const LIMIT_ACCOUNTS = 4;
const STATUS_EMOJI_SIZE = 8 * REM;
const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
stories: PremiumStatus,
double_limits: PremiumLimits,
infinite_reactions: PremiumReactions,
premium_stickers: PremiumStickers,
animated_emoji: PremiumEmoji,
no_ads: PremiumAds,
voice_to_text: PremiumVoice,
profile_badge: PremiumBadge,
faster_download: PremiumSpeed,
more_upload: PremiumFile,
advanced_chat_management: PremiumChats,
animated_userpics: PremiumVideo,
emoji_status: PremiumStatus,
translations: PremiumTranslate,
saved_tags: PremiumTags,
last_seen: PremiumLastSeen,
message_privacy: PremiumMessagePrivacy,
effects: PremiumEffects,
ai_compose: PremiumAi,
todo: PremiumBadge,
pm_noforwards: PremiumNoforwards,
};
export type OwnProps = {
isOpen?: boolean;
};
type StateProps = {
currentUserId?: string;
promo?: ApiPremiumPromo;
fromUser?: ApiUser;
fromUserStatusEmoji?: ApiSticker;
fromUserStatusSet?: ApiStickerSet;
toUser?: ApiUser;
initialSection?: ApiPremiumSection;
isPremium?: boolean;
isSuccess?: boolean;
isGift?: boolean;
daysAmount?: number;
gift?: ApiStarGift;
limitChannels: number;
limitPins: number;
limitLinks: number;
limitFolders: number;
limits?: NonNullable<GlobalState['appConfig']>['limits'];
premiumSlug?: string;
premiumBotUsername?: string;
premiumPromoOrder?: ApiPremiumSection[];
};
const PremiumMainModal: FC<OwnProps & StateProps> = ({
isOpen,
currentUserId,
fromUser,
fromUserStatusEmoji,
fromUserStatusSet,
promo,
initialSection,
isPremium,
limitChannels,
limitLinks,
limitFolders,
limitPins,
limits,
premiumSlug,
premiumBotUsername,
isSuccess,
isGift,
toUser,
daysAmount,
premiumPromoOrder,
gift,
}) => {
const dialogRef = useRef<HTMLDivElement>();
const {
closePremiumModal, openInvoice, requestConfetti, openTelegramLink, loadStickers, openStickerSet,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
const [isHeaderHidden, setIsHeaderHidden] = useState(true);
const [currentSection, setCurrentSection] = useState<ApiPremiumSection | undefined>(initialSection);
const [selectedSubscriptionOption, setSelectedSubscriptionOption] = useState<ApiPremiumSubscriptionOption>();
useEffect(() => {
if (!isOpen) {
setIsHeaderHidden(true);
setCurrentSection(undefined);
} else if (initialSection) {
setCurrentSection(initialSection);
}
}, [isOpen, initialSection]);
const handleOpenSection = useLastCallback((section: ApiPremiumSection) => {
setCurrentSection(section);
});
const handleResetSection = useLastCallback(() => {
setCurrentSection(undefined);
});
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop } = e.currentTarget;
setIsHeaderHidden(scrollTop <= 150);
}
const handleClickWithStartParam = useLastCallback((startParam?: string) => {
const dialog = dialogRef.current;
if (!dialog) return;
if (premiumSlug) {
openInvoice({
type: 'slug',
slug: premiumSlug,
});
} else if (premiumBotUsername) {
openTelegramLink({
url: `${TME_LINK_PREFIX}${premiumBotUsername}?start=${startParam || 'promo'}`,
});
closePremiumModal();
}
});
const handleClick = useLastCallback(() => {
if (selectedSubscriptionOption) {
handleClickWithStartParam(String(selectedSubscriptionOption.months));
} else {
handleClickWithStartParam();
}
});
const handleChangeSubscriptionOption = useLastCallback((months: number) => {
const foundOption = promo?.options.find((option) => option.months === months);
setSelectedSubscriptionOption(foundOption);
});
const showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
const {
top, left, width, height,
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
requestConfetti({
top,
left,
width,
height,
withStars: true,
});
}
});
useEffect(() => {
if (isSuccess) {
showConfetti();
}
}, [isSuccess, showConfetti]);
useSyncEffect(([prevIsPremium]) => {
if (prevIsPremium === isPremium) return;
showConfetti();
}, [isPremium, showConfetti]);
const filteredSections = useMemo(() => {
if (!premiumPromoOrder) return PREMIUM_FEATURE_SECTIONS;
return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section));
}, [premiumPromoOrder]);
useEffect(() => {
if (!fromUserStatusEmoji || fromUserStatusSet) return;
loadStickers({
stickerSetInfo: fromUserStatusEmoji.stickerSetInfo,
});
}, [loadStickers, fromUserStatusEmoji, fromUserStatusSet]);
useEffect(() => {
const [defaultOption] = promo?.options ?? [];
setSelectedSubscriptionOption(defaultOption);
}, [promo]);
const handleOpenStatusSet = useLastCallback(() => {
if (!fromUserStatusSet) return;
openStickerSet({
stickerSetInfo: fromUserStatusSet,
});
});
const fullMonthlyAmount = useMemo(() => {
const monthOption = promo?.options.find((option) => option.months === 1);
if (!monthOption) {
return undefined;
}
return Number(monthOption.amount);
}, [promo]);
const subscribeButtonText = useMemo(() => {
if (!selectedSubscriptionOption) {
return undefined;
}
const { amount, months, currency } = selectedSubscriptionOption;
const perMonthPrice = Math.floor(amount / months);
return formatCurrency(
lang,
perMonthPrice,
currency,
);
}, [selectedSubscriptionOption, lang]);
if (!promo || (fromUserStatusEmoji && !fromUserStatusSet)) return undefined;
function getHeaderText() {
if (gift) {
return lang('PremiumGiftHeader');
}
if (isGift) {
const formattedDuration = daysAmount ? formatCountdownDays(lang, daysAmount) : '';
return renderText(
fromUser?.id === currentUserId
? lang('DialogTitlePremiumGiftSentTo', { user: getUserFullName(toUser), amount: formattedDuration })
: lang('DialogTitlePremiumGiftReceivedFrom', { user: getUserFullName(fromUser), amount: formattedDuration }),
['simple_markdown', 'emoji'],
);
}
if (fromUserStatusSet && fromUser) {
const template = oldLang('lng_premium_emoji_status_title').replace('{user}', getUserFullName(fromUser)!);
const [first, second] = template.split('{link}');
const emoji = fromUserStatusSet.thumbCustomEmojiId ? (
<CustomEmoji className={styles.stickerSetLinkIcon} documentId={fromUserStatusSet.thumbCustomEmojiId} />
) : undefined;
const link = (
<span className={styles.stickerSetLink} onClick={handleOpenStatusSet}>
{emoji}
{renderText(fromUserStatusSet.title)}
</span>
);
return [renderText(first), link, renderText(second)];
}
return renderText(
fromUser
? oldLang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
: oldLang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium'),
['simple_markdown', 'emoji'],
);
}
function getHeaderDescription() {
if (gift && gift.type !== 'starGiftUnique' && gift.perUserTotal) {
return lang('DescriptionGiftPremiumRequired2', { count: gift.perUserTotal }, {
pluralValue: gift.perUserTotal,
});
}
if (isGift) {
return fromUser?.id === currentUserId
? oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
: oldLang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
}
if (fromUserStatusSet) {
return oldLang('TelegramPremiumUserStatusDialogSubtitle');
}
return fromUser
? oldLang('TelegramPremiumUserDialogSubtitle')
: oldLang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
}
function renderHeader() {
if (gift) {
const giftSticker = getStickerFromGift(gift);
return (
<ParticlesHeader
model="sticker"
sticker={giftSticker}
color="purple"
title={getHeaderText()}
description={renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
className={styles.giftParticlesHeader}
/>
);
}
if (!fromUserStatusEmoji) {
return (
<ParticlesHeader
model="swaying-star"
color="purple"
title={getHeaderText()}
description={renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
className={styles.starParticlesHeader}
/>
);
}
return (
<>
<CustomEmoji
className={styles.statusEmoji}
onClick={handleOpenStatusSet}
documentId={fromUserStatusEmoji.id}
isBig
size={STATUS_EMOJI_SIZE}
/>
<h2 className={buildClassName(styles.headerText, fromUserStatusSet && styles.stickerSetText)}>
{getHeaderText()}
</h2>
<div className={styles.description}>
{renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
</div>
</>
);
}
function renderFooterText() {
if (!promo || (isGift && fromUser?.id === currentUserId)) {
return undefined;
}
return (
<div className={styles.footerText} dir={lang.isRtl ? 'rtl' : undefined}>
{renderTextWithEntities({
text: promo.statusText,
entities: promo.statusEntities,
})}
</div>
);
}
function renderSubscriptionOptions() {
return (
<div className={styles.subscriptionOptions}>
{promo?.options
.map((option) => (
<PremiumSubscriptionOption
className={styles.subscriptionOption}
key={option.amount}
option={option}
onChange={handleChangeSubscriptionOption}
fullMonthlyAmount={fullMonthlyAmount}
checked={selectedSubscriptionOption?.months === option.months}
/>
))}
</div>
);
}
return (
<Modal
className={styles.root}
onClose={closePremiumModal}
isOpen={isOpen}
dialogRef={dialogRef}
hasAbsoluteCloseButton={!currentSection}
>
<Transition name="slide" activeKey={currentSection ? 1 : 0} className={styles.transition}>
{!currentSection ? (
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
{renderHeader()}
{!isPremium && !isGift && renderSubscriptionOptions()}
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<h2 className={styles.premiumHeaderText}>
{oldLang('TelegramPremium')}
</h2>
</div>
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
{filteredSections.map((section, index) => {
const shouldUseNewLang = section === 'todo' || section === 'pm_noforwards' || section === 'ai_compose';
return (
<PremiumFeatureItem
key={section}
title={shouldUseNewLang
? lang(PREMIUM_FEATURE_TITLES[section] as keyof LangPair)
: oldLang(PREMIUM_FEATURE_TITLES[section])}
text={section === 'double_limits'
? oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section],
[limitChannels, limitFolders, limitPins, limitLinks, LIMIT_ACCOUNTS])
: shouldUseNewLang
? lang(PREMIUM_FEATURE_DESCRIPTIONS[section] as keyof LangPair)
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
index={index}
count={filteredSections.length}
section={section}
onClick={handleOpenSection}
/>
);
})}
<div
className={buildClassName(styles.footerText, styles.primaryFooterText)}
dir={lang.isRtl ? 'rtl' : undefined}
>
<p>
{renderText(oldLang('AboutPremiumDescription'), ['simple_markdown'])}
</p>
<p>
{renderText(oldLang('AboutPremiumDescription2'), ['simple_markdown'])}
</p>
</div>
{renderFooterText()}
</div>
{!isPremium && selectedSubscriptionOption && (
<div className={styles.footer}>
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
{oldLang('SubscribeToPremium', subscribeButtonText)}
</Button>
</div>
)}
</div>
) : (
<PremiumFeatureModal
initialSection={currentSection}
onBack={handleResetSection}
promo={promo}
onClickSubscribe={handleClickWithStartParam}
isPremium={isPremium}
limits={limits}
premiumPromoOrder={premiumPromoOrder}
subscriptionOption={selectedSubscriptionOption}
/>
)}
</Transition>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
const {
premiumModal,
} = selectTabState(global);
const fromUser = premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined;
const fromUserStatusEmoji = fromUser?.emojiStatus ? selectCustomEmoji(global, fromUser.emojiStatus.documentId)
: undefined;
const fromUserStatusSet = fromUserStatusEmoji ? selectStickerSet(global, fromUserStatusEmoji.stickerSetInfo)
: undefined;
return {
currentUserId: global.currentUserId,
promo: premiumModal?.promo,
isSuccess: premiumModal?.isSuccess,
isGift: premiumModal?.isGift,
daysAmount: premiumModal?.daysAmount,
gift: premiumModal?.gift,
fromUser,
fromUserStatusEmoji,
fromUserStatusSet,
toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined,
initialSection: premiumModal?.initialSection,
isPremium: selectIsCurrentUserPremium(global),
limitChannels: selectPremiumLimit(global, 'channels'),
limitFolders: selectPremiumLimit(global, 'dialogFilters'),
limitPins: selectPremiumLimit(global, 'dialogFolderPinned'),
limitLinks: selectPremiumLimit(global, 'channelsPublic'),
limits: global.appConfig.limits,
premiumSlug: global.appConfig.premiumInvoiceSlug,
premiumBotUsername: global.appConfig.premiumBotUsername,
premiumPromoOrder: global.appConfig.premiumPromoOrder,
};
})(PremiumMainModal));