Stars: Support bot subscriptions (#5261)
This commit is contained in:
parent
c6d30e8cb8
commit
34b3df1ca6
@ -233,6 +233,7 @@ export function buildApiInvoice(invoice: GramJs.Invoice): ApiInvoice {
|
|||||||
phoneToProvider,
|
phoneToProvider,
|
||||||
shippingAddressRequested,
|
shippingAddressRequested,
|
||||||
flexible,
|
flexible,
|
||||||
|
subscriptionPeriod,
|
||||||
} = invoice;
|
} = invoice;
|
||||||
|
|
||||||
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
|
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
|
||||||
@ -258,6 +259,7 @@ export function buildApiInvoice(invoice: GramJs.Invoice): ApiInvoice {
|
|||||||
isPhoneSentToProvider: phoneToProvider,
|
isPhoneSentToProvider: phoneToProvider,
|
||||||
isShippingAddressRequested: shippingAddressRequested,
|
isShippingAddressRequested: shippingAddressRequested,
|
||||||
isFlexible: flexible,
|
isFlexible: flexible,
|
||||||
|
subscriptionPeriod,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,9 +561,14 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
|||||||
|
|
||||||
export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription): ApiStarsSubscription {
|
export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription): ApiStarsSubscription {
|
||||||
const {
|
const {
|
||||||
id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance,
|
id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance, botCanceled, photo, title,
|
||||||
|
invoiceSlug,
|
||||||
} = subscription;
|
} = subscription;
|
||||||
|
|
||||||
|
if (photo) {
|
||||||
|
addWebDocumentToLocalDb(photo);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
peerId: getApiChatIdFromMtpPeer(peer),
|
peerId: getApiChatIdFromMtpPeer(peer),
|
||||||
@ -571,6 +578,10 @@ export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription
|
|||||||
canRefulfill,
|
canRefulfill,
|
||||||
hasMissingBalance: missingBalance,
|
hasMissingBalance: missingBalance,
|
||||||
chatInviteHash,
|
chatInviteHash,
|
||||||
|
hasBotCancelled: botCanceled,
|
||||||
|
title,
|
||||||
|
photo: photo && buildApiWebDocument(photo),
|
||||||
|
invoiceSlug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -342,6 +342,7 @@ export interface ApiInvoice {
|
|||||||
currency: string;
|
currency: string;
|
||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
isRecurring?: boolean;
|
isRecurring?: boolean;
|
||||||
|
subscriptionPeriod?: number;
|
||||||
termsUrl?: string;
|
termsUrl?: string;
|
||||||
maxTipAmount?: number;
|
maxTipAmount?: number;
|
||||||
suggestedTipAmounts?: number[];
|
suggestedTipAmounts?: number[];
|
||||||
|
|||||||
@ -375,6 +375,10 @@ export interface ApiStarsSubscription {
|
|||||||
canRefulfill?: true;
|
canRefulfill?: true;
|
||||||
hasMissingBalance?: true;
|
hasMissingBalance?: true;
|
||||||
chatInviteHash?: string;
|
chatInviteHash?: string;
|
||||||
|
hasBotCancelled?: true;
|
||||||
|
title?: string;
|
||||||
|
photo?: ApiWebDocument;
|
||||||
|
invoiceSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStarTopupOption {
|
export interface ApiStarTopupOption {
|
||||||
|
|||||||
@ -1346,6 +1346,7 @@
|
|||||||
"LimitedGiftsCategory" = "Limited";
|
"LimitedGiftsCategory" = "Limited";
|
||||||
"PremiumGiftDescription" = "Premium";
|
"PremiumGiftDescription" = "Premium";
|
||||||
"SendPaidReaction" = "Send ⭐️{amount}";
|
"SendPaidReaction" = "Send ⭐️{amount}";
|
||||||
|
"StarsPay" = "Confirm and Pay {amount}";
|
||||||
"StarsReactionTerms" = "By sending Stars you agree to the {link}";
|
"StarsReactionTerms" = "By sending Stars you agree to the {link}";
|
||||||
"StarsReactionLinkText" = "Terms of Service";
|
"StarsReactionLinkText" = "Terms of Service";
|
||||||
"StarsReactionLink" = "https://telegram.org/tos/stars";
|
"StarsReactionLink" = "https://telegram.org/tos/stars";
|
||||||
@ -1392,3 +1393,6 @@
|
|||||||
"BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?";
|
"BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?";
|
||||||
"BotSuggestedStatusTitle" = "Set Emoji Status";
|
"BotSuggestedStatusTitle" = "Set Emoji Status";
|
||||||
"BotSuggestedStatusUpdated" = "Your emoji status is updated.";
|
"BotSuggestedStatusUpdated" = "Your emoji status is updated.";
|
||||||
|
"StarsSubscribeBotText_one" = "Do you want to subscribe to **{name}** in **{bot}** for **{amount}** star per month?"
|
||||||
|
"StarsSubscribeBotText_other" = "Do you want to subscribe to **{name}** in **{bot}** for **{amount}** stars per month?"
|
||||||
|
"StarsSubscribeBotButtonMonth" = "Subscribe for {amount} / month";
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiPeer } from '../../api/types';
|
import type { ApiPeer, ApiWebDocument } from '../../api/types';
|
||||||
import type { CustomPeer } from '../../types';
|
import type { CustomPeer } from '../../types';
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import Avatar from './Avatar';
|
import Avatar, { type AvatarSize } from './Avatar';
|
||||||
import Icon from './icons/Icon';
|
import Icon from './icons/Icon';
|
||||||
|
|
||||||
import styles from './PeerBadge.module.scss';
|
import styles from './PeerBadge.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
peer: ApiPeer | CustomPeer;
|
peer?: ApiPeer | CustomPeer;
|
||||||
|
avatarWebPhoto?: ApiWebDocument;
|
||||||
|
avatarSize?: AvatarSize;
|
||||||
text?: string;
|
text?: string;
|
||||||
badgeText?: string;
|
badgeText?: string;
|
||||||
badgeIcon?: IconName;
|
badgeIcon?: IconName;
|
||||||
@ -24,7 +26,9 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PeerBadge = ({
|
const PeerBadge = ({
|
||||||
peer,
|
peer: avatarPeer,
|
||||||
|
avatarWebPhoto,
|
||||||
|
avatarSize,
|
||||||
text,
|
text,
|
||||||
badgeText,
|
badgeText,
|
||||||
badgeIcon,
|
badgeIcon,
|
||||||
@ -40,7 +44,7 @@ const PeerBadge = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className={styles.top}>
|
<div className={styles.top}>
|
||||||
<Avatar size="large" peer={peer} />
|
<Avatar size={avatarSize} peer={avatarPeer} webPhoto={avatarWebPhoto} />
|
||||||
{badgeText && (
|
{badgeText && (
|
||||||
<div className={buildClassName(styles.badge, badgeClassName)}>
|
<div className={buildClassName(styles.badge, badgeClassName)}>
|
||||||
{badgeIcon && <Icon name={badgeIcon} className={badgeIconClassName} />}
|
{badgeIcon && <Icon name={badgeIcon} className={badgeIconClassName} />}
|
||||||
|
|||||||
@ -109,6 +109,10 @@
|
|||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.botItem {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.hiddenHeader {
|
.hiddenHeader {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
@ -191,15 +195,9 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paymentAmount {
|
|
||||||
display: flex;
|
|
||||||
line-height: 1.125;
|
|
||||||
gap: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paymentButton {
|
.paymentButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.125rem;
|
align-items: center;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,3 +223,20 @@
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amountBadge {
|
||||||
|
background-image: var(--stars-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMore {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreIcon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const StarsBalanceModal = ({
|
|||||||
modal, starsBalanceState, canBuyPremium,
|
modal, starsBalanceState, canBuyPremium,
|
||||||
}: OwnProps & StateProps) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingPickerModal, openInvoice,
|
closeStarsBalanceModal, loadStarsTransactions, loadStarsSubscriptions, openStarsGiftingPickerModal, openInvoice,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const { balance, history, subscriptions } = starsBalanceState || {};
|
const { balance, history, subscriptions } = starsBalanceState || {};
|
||||||
@ -154,6 +154,10 @@ const StarsBalanceModal = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleLoadMoreSubscriptions = useLastCallback(() => {
|
||||||
|
loadStarsSubscriptions();
|
||||||
|
});
|
||||||
|
|
||||||
const openStarsGiftingPickerModalHandler = useLastCallback(() => {
|
const openStarsGiftingPickerModalHandler = useLastCallback(() => {
|
||||||
openStarsGiftingPickerModal({});
|
openStarsGiftingPickerModal({});
|
||||||
});
|
});
|
||||||
@ -240,6 +244,19 @@ const StarsBalanceModal = ({
|
|||||||
subscription={subscription}
|
subscription={subscription}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{subscriptions?.nextOffset && (
|
||||||
|
<Button
|
||||||
|
isText
|
||||||
|
disabled={subscriptions.isLoading}
|
||||||
|
size="smaller"
|
||||||
|
noForcedUpperCase
|
||||||
|
className={styles.loadMore}
|
||||||
|
onClick={handleLoadMoreSubscriptions}
|
||||||
|
>
|
||||||
|
<Icon name="down" className={styles.loadMoreIcon} />
|
||||||
|
{oldLang('StarMySubscriptionsExpand')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
selectChat, selectChatMessage, selectUser,
|
selectChat, selectChatMessage, selectUser,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||||
|
import { formatInteger } from '../../../util/textFormat';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -21,6 +23,8 @@ import usePrevious from '../../../hooks/usePrevious';
|
|||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import StarIcon from '../../common/icons/StarIcon';
|
import StarIcon from '../../common/icons/StarIcon';
|
||||||
|
import PeerBadge from '../../common/PeerBadge';
|
||||||
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import SafeLink from '../../common/SafeLink';
|
import SafeLink from '../../common/SafeLink';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
@ -58,6 +62,8 @@ const StarPaymentModal = ({
|
|||||||
|
|
||||||
const { form, subscriptionInfo } = renderingModal || {};
|
const { form, subscriptionInfo } = renderingModal || {};
|
||||||
const amount = form?.invoice?.totalAmount || subscriptionInfo?.subscriptionPricing?.amount;
|
const amount = form?.invoice?.totalAmount || subscriptionInfo?.subscriptionPricing?.amount;
|
||||||
|
const isBotSubscription = Boolean(form?.invoice.subscriptionPeriod);
|
||||||
|
const canShowPeerItem = !subscriptionInfo?.subscriptionPricing;
|
||||||
|
|
||||||
const photo = form?.photo;
|
const photo = form?.photo;
|
||||||
|
|
||||||
@ -102,8 +108,21 @@ const StarPaymentModal = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBotSubscription) {
|
||||||
|
return lang('StarsSubscribeBotText', {
|
||||||
|
name: form.title,
|
||||||
|
amount,
|
||||||
|
bot: botName,
|
||||||
|
}, {
|
||||||
|
pluralValue: amount!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return oldLang('Stars.Transfer.Info', [form!.title, botName, starsText]);
|
return oldLang('Stars.Transfer.Info', [form!.title, botName, starsText]);
|
||||||
}, [renderingModal, bot, oldLang, amount, paidMediaMessage, subscriptionInfo, form, paidMediaChat, lang]);
|
}, [
|
||||||
|
renderingModal?.inputInvoice, bot, oldLang, amount, paidMediaMessage, subscriptionInfo, isBotSubscription, form,
|
||||||
|
paidMediaChat, lang,
|
||||||
|
]);
|
||||||
|
|
||||||
const disclaimerText = useMemo(() => {
|
const disclaimerText = useMemo(() => {
|
||||||
if (subscriptionInfo) {
|
if (subscriptionInfo) {
|
||||||
@ -160,25 +179,31 @@ const StarPaymentModal = ({
|
|||||||
<StarIcon type="gold" size="adaptive" className={styles.avatarStar} />
|
<StarIcon type="gold" size="adaptive" className={styles.avatarStar} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<PeerBadge
|
||||||
<Avatar peer={bot} size="giant" />
|
peer={!photo ? bot : undefined}
|
||||||
{photo && <Avatar className={styles.paymentPhoto} webPhoto={photo} size="giant" />}
|
avatarWebPhoto={photo}
|
||||||
</>
|
avatarSize="giant"
|
||||||
|
badgeIcon="star"
|
||||||
|
badgeText={formatInteger(amount!)}
|
||||||
|
badgeClassName={styles.amountBadge}
|
||||||
|
className={styles.paymentPhoto}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} />
|
<img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} />
|
||||||
</div>
|
</div>
|
||||||
<h2 className={styles.headerText}>
|
<h2 className={styles.headerText}>
|
||||||
{inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}
|
{inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
|
{canShowPeerItem && <PickerSelectedItem className={styles.botItem} peerId={form?.botId} />}
|
||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
|
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
|
||||||
</div>
|
</div>
|
||||||
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
|
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
|
||||||
{oldLang('Stars.Transfer.Pay')}
|
{lang(isBotSubscription ? 'StarsSubscribeBotButtonMonth' : 'StarsPay', {
|
||||||
<div className={styles.paymentAmount}>
|
amount: formatStarsAsIcon(lang, amount!, true),
|
||||||
{amount}
|
}, {
|
||||||
<StarIcon className={styles.paymentButtonStar} size="small" />
|
withNodes: true,
|
||||||
</div>
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
{disclaimerText && (
|
{disclaimerText && (
|
||||||
<div className={buildClassName(styles.disclaimer, styles.smallerText)}>
|
<div className={buildClassName(styles.disclaimer, styles.smallerText)}>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/paym
|
|||||||
|
|
||||||
export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) {
|
export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) {
|
||||||
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
||||||
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
|
if (transaction.subscriptionPeriod) return transaction.title || lang('StarSubscriptionPurchase');
|
||||||
if (transaction.isReaction) return lang('StarsReactionsSent');
|
if (transaction.isReaction) return lang('StarsReactionsSent');
|
||||||
if (transaction.giveawayPostId) return lang('StarsGiveawayPrizeReceived');
|
if (transaction.giveawayPostId) return lang('StarsGiveawayPrizeReceived');
|
||||||
if (transaction.isMyGift) return lang('StarsGiftSent');
|
if (transaction.isMyGift) return lang('StarsGiftSent');
|
||||||
|
|||||||
@ -19,6 +19,13 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.125rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -37,7 +44,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title, .description {
|
.title, .description, .subtitle {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ function selectProvidedPeer(peerId: string) {
|
|||||||
const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
|
const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
|
||||||
const { openStarsSubscriptionModal } = getActions();
|
const { openStarsSubscriptionModal } = getActions();
|
||||||
const {
|
const {
|
||||||
peerId, pricing, until, isCancelled,
|
peerId, pricing, until, isCancelled, title, photo,
|
||||||
} = subscription;
|
} = subscription;
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
@ -57,6 +57,12 @@ const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<h3 className={styles.title}>{getSenderTitle(lang, peer)}</h3>
|
<h3 className={styles.title}>{getSenderTitle(lang, peer)}</h3>
|
||||||
|
{title && (
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
{photo && <Avatar webPhoto={photo} size="micro" />}
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
{lang(
|
{lang(
|
||||||
hasExpired ? 'StarsSubscriptionExpired'
|
hasExpired ? 'StarsSubscriptionExpired'
|
||||||
|
|||||||
@ -21,6 +21,13 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
import type { TabState } from '../../../../global/types';
|
import type { TabState } from '../../../../global/types';
|
||||||
|
|
||||||
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
||||||
|
import { isApiPeerUser } from '../../../../global/helpers/peers';
|
||||||
import {
|
import {
|
||||||
selectPeer,
|
selectPeer,
|
||||||
} from '../../../../global/selectors';
|
} from '../../../../global/selectors';
|
||||||
@ -46,6 +47,7 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
changeStarsSubscription,
|
changeStarsSubscription,
|
||||||
checkChatInvite,
|
checkChatInvite,
|
||||||
loadStarStatus,
|
loadStarStatus,
|
||||||
|
openInvoice,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -69,7 +71,8 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
return 'renew';
|
return 'renew';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isActive) {
|
const canRestart = subscription.chatInviteHash || subscription.invoiceSlug;
|
||||||
|
if (!isActive && canRestart) {
|
||||||
return 'restart';
|
return 'restart';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +90,14 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'restart': {
|
case 'restart': {
|
||||||
checkChatInvite({ hash: subscription.chatInviteHash! });
|
if (subscription.chatInviteHash) {
|
||||||
|
checkChatInvite({ hash: subscription.chatInviteHash });
|
||||||
|
} else if (subscription.invoiceSlug) {
|
||||||
|
openInvoice({
|
||||||
|
type: 'slug',
|
||||||
|
slug: subscription.invoiceSlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
loadStarStatus();
|
loadStarStatus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -109,13 +119,15 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pricing, until, isCancelled, canRefulfill,
|
pricing, until, isCancelled, canRefulfill, photo, title, hasBotCancelled,
|
||||||
} = subscription;
|
} = subscription;
|
||||||
|
|
||||||
|
const isBotSubscription = isApiPeerUser(peer);
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<div className={buildClassName(styles.header, styles.starsHeader)}>
|
<div className={buildClassName(styles.header, styles.starsHeader)}>
|
||||||
<div className={styles.avatarWrapper}>
|
<div className={styles.avatarWrapper}>
|
||||||
<Avatar peer={peer} size="jumbo" />
|
<Avatar peer={!photo ? peer : undefined} webPhoto={photo} size="jumbo" />
|
||||||
<StarIcon className={styles.subscriptionStar} type="gold" size="adaptive" />
|
<StarIcon className={styles.subscriptionStar} type="gold" size="adaptive" />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
@ -124,7 +136,7 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
alt=""
|
alt=""
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
<h1 className={styles.title}>{oldLang('StarsSubscriptionTitle')}</h1>
|
<h1 className={styles.title}>{title || oldLang('StarsSubscriptionTitle')}</h1>
|
||||||
<p className={styles.amount}>
|
<p className={styles.amount}>
|
||||||
{lang('StarsPerMonth', {
|
{lang('StarsPerMonth', {
|
||||||
amount: pricing.amount,
|
amount: pricing.amount,
|
||||||
@ -141,10 +153,17 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
const tableData: TableData = [];
|
const tableData: TableData = [];
|
||||||
|
|
||||||
tableData.push([
|
tableData.push([
|
||||||
oldLang('StarsSubscriptionChannel'),
|
oldLang(isBotSubscription ? 'StarsSubscriptionBot' : 'StarsSubscriptionChannel'),
|
||||||
{ chatId: peer.id },
|
{ chatId: peer.id },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
tableData.push([
|
||||||
|
oldLang('StarsSubscriptionBotProduct'),
|
||||||
|
title,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
const hasExpired = until < Date.now() / 1000;
|
const hasExpired = until < Date.now() / 1000;
|
||||||
tableData.push([
|
tableData.push([
|
||||||
oldLang(hasExpired ? 'StarsSubscriptionUntilExpired'
|
oldLang(hasExpired ? 'StarsSubscriptionUntilExpired'
|
||||||
@ -162,7 +181,9 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
|
|||||||
<span className={styles.footer}>
|
<span className={styles.footer}>
|
||||||
<p className={styles.secondary}>{footerTos}</p>
|
<p className={styles.secondary}>{footerTos}</p>
|
||||||
{isCancelled && (
|
{isCancelled && (
|
||||||
<p className={styles.danger}>{oldLang('StarsSubscriptionCancelledText')}</p>
|
<p className={styles.danger}>
|
||||||
|
{oldLang(hasBotCancelled ? 'StarsSubscriptionBotCancelledText' : 'StarsSubscriptionCancelledText')}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
{canRefulfill && (
|
{canRefulfill && (
|
||||||
<p className={styles.secondary}>
|
<p className={styles.secondary}>
|
||||||
|
|||||||
@ -37,6 +37,13 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
.tid {
|
.tid {
|
||||||
font-family: var(--font-family-monospace);
|
font-family: var(--font-family-monospace);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
appendStarsSubscriptions,
|
appendStarsSubscriptions,
|
||||||
appendStarsTransactions,
|
appendStarsTransactions,
|
||||||
updateStarsBalance,
|
updateStarsBalance,
|
||||||
|
updateStarsSubscriptionLoading,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectPeer,
|
selectPeer,
|
||||||
@ -179,6 +180,9 @@ addActionHandler('loadStarsSubscriptions', async (global): Promise<void> => {
|
|||||||
const offset = subscriptions?.nextOffset;
|
const offset = subscriptions?.nextOffset;
|
||||||
if (subscriptions && !offset) return; // Already loaded all
|
if (subscriptions && !offset) return; // Already loaded all
|
||||||
|
|
||||||
|
global = updateStarsSubscriptionLoading(global, true);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
const result = await callApi('fetchStarsSubscriptions', {
|
const result = await callApi('fetchStarsSubscriptions', {
|
||||||
offset: offset || '',
|
offset: offset || '',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -42,7 +42,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
case 'updateStarPaymentStateCompleted': {
|
case 'updateStarPaymentStateCompleted': {
|
||||||
const { paymentState, tabId } = update;
|
const { paymentState, tabId } = update;
|
||||||
const { inputInvoice, subscriptionInfo } = paymentState;
|
const { inputInvoice, subscriptionInfo, form } = paymentState;
|
||||||
if (inputInvoice?.type === 'chatInviteSubscription' && subscriptionInfo) {
|
if (inputInvoice?.type === 'chatInviteSubscription' && subscriptionInfo) {
|
||||||
const amount = subscriptionInfo.subscriptionPricing!.amount;
|
const amount = subscriptionInfo.subscriptionPricing!.amount;
|
||||||
|
|
||||||
@ -57,6 +57,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form?.invoice.subscriptionPeriod) {
|
||||||
|
const amount = form.invoice.totalAmount;
|
||||||
|
actions.showNotification({
|
||||||
|
tabId,
|
||||||
|
title: langProvider.oldTranslate('StarsSubscriptionCompleted'),
|
||||||
|
message: langProvider.oldTranslate('StarsSubscriptionCompletedText', [
|
||||||
|
amount,
|
||||||
|
form.title,
|
||||||
|
], undefined, amount),
|
||||||
|
icon: 'star',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (inputInvoice?.type === 'giftcode') {
|
if (inputInvoice?.type === 'giftcode') {
|
||||||
if (!inputInvoice.userIds) {
|
if (!inputInvoice.userIds) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type {
|
|||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { PaymentStep, ShippingOption } from '../../types';
|
import type { PaymentStep, ShippingOption } from '../../types';
|
||||||
import type {
|
import type {
|
||||||
GlobalState, StarsTransactionType, TabArgs, TabState,
|
GlobalState, StarsSubscriptions, StarsTransactionType, TabArgs, TabState,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
@ -182,7 +182,7 @@ export function appendStarsSubscriptions<T extends GlobalState>(
|
|||||||
const newObject = {
|
const newObject = {
|
||||||
list: (global.stars.subscriptions?.list || []).concat(subscriptions),
|
list: (global.stars.subscriptions?.list || []).concat(subscriptions),
|
||||||
nextOffset,
|
nextOffset,
|
||||||
};
|
} satisfies StarsSubscriptions;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
@ -193,6 +193,26 @@ export function appendStarsSubscriptions<T extends GlobalState>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateStarsSubscriptionLoading<T extends GlobalState>(
|
||||||
|
global: T, isLoading: boolean,
|
||||||
|
): T {
|
||||||
|
const subscriptions = global.stars?.subscriptions;
|
||||||
|
if (!subscriptions) {
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
stars: {
|
||||||
|
...global.stars,
|
||||||
|
subscriptions: {
|
||||||
|
...subscriptions,
|
||||||
|
isLoading,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function openStarsTransactionModal<T extends GlobalState>(
|
export function openStarsTransactionModal<T extends GlobalState>(
|
||||||
global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
|
|||||||
@ -190,6 +190,7 @@ export type StarsTransactionHistory = Record<StarsTransactionType, {
|
|||||||
export type StarsSubscriptions = {
|
export type StarsSubscriptions = {
|
||||||
list: ApiStarsSubscription[];
|
list: ApiStarsSubscription[];
|
||||||
nextOffset?: string;
|
nextOffset?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfettiStyle = 'poppers' | 'top-down';
|
export type ConfettiStyle = 'poppers' | 'top-down';
|
||||||
|
|||||||
@ -292,8 +292,10 @@ body:not(.is-ios) {
|
|||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.star-amount-icon {
|
// Increase specificity to override the default icon style
|
||||||
margin-inline-start: 0.5em; // Prevent sticking to the text without using `white-space: pre`
|
.star-amount-icon.star-amount-icon {
|
||||||
|
line-height: inherit; // Vertical centring
|
||||||
|
margin-inline-start: 0.375em; // Prevent sticking to the text without using `white-space: pre`
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-canvas-container {
|
.shared-canvas-container {
|
||||||
|
|||||||
11
src/types/language.d.ts
vendored
11
src/types/language.d.ts
vendored
@ -1509,6 +1509,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
|||||||
'SendPaidReaction': {
|
'SendPaidReaction': {
|
||||||
'amount': V;
|
'amount': V;
|
||||||
};
|
};
|
||||||
|
'StarsPay': {
|
||||||
|
'amount': V;
|
||||||
|
};
|
||||||
'StarsReactionTerms': {
|
'StarsReactionTerms': {
|
||||||
'link': V;
|
'link': V;
|
||||||
};
|
};
|
||||||
@ -1555,6 +1558,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
|||||||
'BotSuggestedStatus': {
|
'BotSuggestedStatus': {
|
||||||
'bot': V;
|
'bot': V;
|
||||||
};
|
};
|
||||||
|
'StarsSubscribeBotButtonMonth': {
|
||||||
|
'amount': V;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangPairPlural {
|
export interface LangPairPlural {
|
||||||
@ -1731,6 +1737,11 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
|
|||||||
'chat': V;
|
'chat': V;
|
||||||
'amount': V;
|
'amount': V;
|
||||||
};
|
};
|
||||||
|
'StarsSubscribeBotText': {
|
||||||
|
'name': V;
|
||||||
|
'bot': V;
|
||||||
|
'amount': V;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type RegularLangKey = keyof LangPair;
|
export type RegularLangKey = keyof LangPair;
|
||||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||||
|
|||||||
@ -76,6 +76,7 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
|
|||||||
PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired',
|
PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired',
|
||||||
|
|
||||||
STARGIFT_CONVERT_TOO_OLD: 'This gift no longer can be converted to Stars',
|
STARGIFT_CONVERT_TOO_OLD: 'This gift no longer can be converted to Stars',
|
||||||
|
SUBSCRIPTION_ALREADY_ACTIVE: 'You are already subscribed',
|
||||||
|
|
||||||
PEERS_LIST_EMPTY: 'No chats are added to the list',
|
PEERS_LIST_EMPTY: 'No chats are added to the list',
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,21 @@ import type { LangFn } from './types';
|
|||||||
|
|
||||||
import { STARS_ICON_PLACEHOLDER } from '../../config';
|
import { STARS_ICON_PLACEHOLDER } from '../../config';
|
||||||
|
|
||||||
|
import Icon from '../../components/common/icons/Icon';
|
||||||
import StarIcon from '../../components/common/icons/StarIcon';
|
import StarIcon from '../../components/common/icons/StarIcon';
|
||||||
|
|
||||||
export function formatStarsAsText(lang: LangFn, amount: number) {
|
export function formatStarsAsText(lang: LangFn, amount: number) {
|
||||||
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatStarsAsIcon(lang: LangFn, amount: number) {
|
export function formatStarsAsIcon(lang: LangFn, amount: number, asFont?: boolean) {
|
||||||
|
const icon = asFont
|
||||||
|
? <Icon name="star" className="star-amount-icon" />
|
||||||
|
: <StarIcon type="gold" className="star-amount-icon" size="adaptive" />;
|
||||||
return lang('StarsAmount', { amount }, {
|
return lang('StarsAmount', { amount }, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: {
|
specialReplacement: {
|
||||||
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" className="star-amount-icon" size="adaptive" />,
|
[STARS_ICON_PLACEHOLDER]: icon,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user