TelegramPWA/src/components/main/premium/PremiumMainModal.tsx
2024-03-01 14:02:56 -05:00

479 lines
16 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
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 { ApiPremiumSection, GlobalState } from '../../../global/types';
import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
import {
selectIsCurrentUserPremium, selectStickerSet,
selectTabState, selectUser,
} from '../../../global/selectors';
import { selectPremiumLimit } from '../../../global/selectors/limits';
import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
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 useSyncEffect from '../../../hooks/useSyncEffect';
import CustomEmoji from '../../common/CustomEmoji';
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 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';
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,
};
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;
monthsAmount?: number;
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,
monthsAmount,
premiumPromoOrder,
}) => {
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const {
closePremiumModal, openInvoice, requestConfetti, openTelegramLink, loadStickers, openStickerSet,
} = getActions();
const lang = useLang();
const [isHeaderHidden, setHeaderHidden] = useState(true);
const [currentSection, setCurrentSection] = useState<ApiPremiumSection | undefined>(initialSection);
const [selectedSubscriptionOption, setSubscriptionOption] = useState<ApiPremiumSubscriptionOption>();
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;
setHeaderHidden(scrollTop <= 150);
}
const handleClickWithStartParam = useLastCallback((startParam?: string) => {
const dialog = dialogRef.current;
if (!dialog) return;
if (premiumSlug) {
openInvoice({
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);
setSubscriptionOption(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,
});
}
});
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 ?? [];
setSubscriptionOption(defaultOption);
}, [promo]);
const handleOpenStatusSet = useLastCallback(() => {
if (!fromUserStatusSet) return;
openStickerSet({
stickerSetInfo: fromUserStatusSet,
});
});
const stickerSetTitle = useMemo(() => {
if (!fromUserStatusSet || !fromUser) return undefined;
const template = lang('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)];
}, [fromUser, fromUserStatusSet, lang]);
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(
perMonthPrice,
currency,
lang.code,
);
}, [selectedSubscriptionOption, lang.code]);
if (!promo || (fromUserStatusEmoji && !fromUserStatusSet)) return undefined;
function getHeaderText() {
if (isGift) {
return fromUser?.id === currentUserId
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', [getUserFullName(toUser), monthsAmount])
: lang('TelegramPremiumUserGiftedPremiumDialogTitle', [getUserFullName(fromUser), monthsAmount]);
}
return fromUser
? lang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
: lang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium');
}
function getHeaderDescription() {
if (isGift) {
return fromUser?.id === currentUserId
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
: lang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
}
if (fromUserStatusSet) {
return lang('TelegramPremiumUserStatusDialogSubtitle');
}
return fromUser
? lang('TelegramPremiumUserDialogSubtitle')
: lang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
}
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}
>
<Transition name="slide" activeKey={currentSection ? 1 : 0} className={styles.transition}>
{!currentSection ? (
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<Button
round
size="smaller"
className={styles.closeButton}
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closePremiumModal()}
ariaLabel={lang('Close')}
>
<i className="icon icon-close" />
</Button>
{fromUserStatusEmoji ? (
<CustomEmoji
className={styles.statusEmoji}
onClick={handleOpenStatusSet}
documentId={fromUserStatusEmoji.id}
isBig
size={STATUS_EMOJI_SIZE}
/>
) : (
<img className={styles.logo} src={PremiumLogo} alt="" draggable={false} />
)}
<h2 className={buildClassName(styles.headerText, fromUserStatusSet && styles.stickerSetText)}>
{fromUserStatusSet ? stickerSetTitle : renderText(getHeaderText(), ['simple_markdown', 'emoji'])}
</h2>
<div className={styles.description}>
{renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
</div>
{!isPremium && renderSubscriptionOptions()}
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<h2 className={styles.premiumHeaderText}>
{lang('TelegramPremium')}
</h2>
</div>
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
{filteredSections.map((section, index) => {
return (
<PremiumFeatureItem
key={section}
title={lang(PREMIUM_FEATURE_TITLES[section])}
text={section === 'double_limits'
? lang(PREMIUM_FEATURE_DESCRIPTIONS[section],
[limitChannels, limitFolders, limitPins, limitLinks, LIMIT_ACCOUNTS])
: lang(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(lang('AboutPremiumDescription'), ['simple_markdown'])}
</p>
<p>
{renderText(lang('AboutPremiumDescription2'), ['simple_markdown'])}
</p>
</div>
{renderFooterText()}
</div>
{!isPremium && selectedSubscriptionOption && (
<div className={styles.footer}>
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
{lang('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): StateProps => {
const {
premiumModal,
} = selectTabState(global);
const fromUser = premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined;
const fromUserStatusEmoji = fromUser?.emojiStatus ? global.customEmojis.byId[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,
monthsAmount: premiumModal?.monthsAmount,
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));