From 34b3df1ca617a0d84bf1e65fb2b288ecccc6fcf1 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:44:04 +0400 Subject: [PATCH] Stars: Support bot subscriptions (#5261) --- src/api/gramjs/apiBuilders/payments.ts | 13 +++++- src/api/types/messages.ts | 1 + src/api/types/payments.ts | 4 ++ src/assets/localization/fallback.strings | 4 ++ src/components/common/PeerBadge.tsx | 14 +++--- .../stars/StarsBalanceModal.module.scss | 29 +++++++++--- .../modals/stars/StarsBalanceModal.tsx | 19 +++++++- .../modals/stars/StarsPaymentModal.tsx | 45 ++++++++++++++----- .../modals/stars/helpers/transaction.ts | 2 +- .../StarsSubscriptionItem.module.scss | 9 +++- .../subscription/StarsSubscriptionItem.tsx | 8 +++- .../StarsSubscriptionModal.module.scss | 7 +++ .../subscription/StarsSubscriptionModal.tsx | 35 ++++++++++++--- .../StarsTransactionModal.module.scss | 7 +++ src/global/actions/api/stars.ts | 4 ++ src/global/actions/apiUpdaters/payments.ts | 15 ++++++- src/global/reducers/payments.ts | 24 +++++++++- src/global/types.ts | 1 + src/styles/index.scss | 6 ++- src/types/language.d.ts | 11 +++++ src/util/getReadableErrorText.ts | 1 + src/util/localization/format.tsx | 8 +++- 22 files changed, 226 insertions(+), 41 deletions(-) diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 338901dea..f20fe1ef3 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -233,6 +233,7 @@ export function buildApiInvoice(invoice: GramJs.Invoice): ApiInvoice { phoneToProvider, shippingAddressRequested, flexible, + subscriptionPeriod, } = invoice; const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({ @@ -258,6 +259,7 @@ export function buildApiInvoice(invoice: GramJs.Invoice): ApiInvoice { isPhoneSentToProvider: phoneToProvider, isShippingAddressRequested: shippingAddressRequested, isFlexible: flexible, + subscriptionPeriod, }; } @@ -559,9 +561,14 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription): ApiStarsSubscription { const { - id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance, + id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance, botCanceled, photo, title, + invoiceSlug, } = subscription; + if (photo) { + addWebDocumentToLocalDb(photo); + } + return { id, peerId: getApiChatIdFromMtpPeer(peer), @@ -571,6 +578,10 @@ export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription canRefulfill, hasMissingBalance: missingBalance, chatInviteHash, + hasBotCancelled: botCanceled, + title, + photo: photo && buildApiWebDocument(photo), + invoiceSlug, }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 707e26f13..cfc431392 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -342,6 +342,7 @@ export interface ApiInvoice { currency: string; isTest?: boolean; isRecurring?: boolean; + subscriptionPeriod?: number; termsUrl?: string; maxTipAmount?: number; suggestedTipAmounts?: number[]; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index ae4ac8508..0e9643c0a 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -375,6 +375,10 @@ export interface ApiStarsSubscription { canRefulfill?: true; hasMissingBalance?: true; chatInviteHash?: string; + hasBotCancelled?: true; + title?: string; + photo?: ApiWebDocument; + invoiceSlug?: string; } export interface ApiStarTopupOption { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 295633a11..da17a0c57 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1346,6 +1346,7 @@ "LimitedGiftsCategory" = "Limited"; "PremiumGiftDescription" = "Premium"; "SendPaidReaction" = "Send ⭐️{amount}"; +"StarsPay" = "Confirm and Pay {amount}"; "StarsReactionTerms" = "By sending Stars you agree to the {link}"; "StarsReactionLinkText" = "Terms of Service"; "StarsReactionLink" = "https://telegram.org/tos/stars"; @@ -1392,3 +1393,6 @@ "BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?"; "BotSuggestedStatusTitle" = "Set Emoji Status"; "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"; diff --git a/src/components/common/PeerBadge.tsx b/src/components/common/PeerBadge.tsx index a6868833b..8e8a261ea 100644 --- a/src/components/common/PeerBadge.tsx +++ b/src/components/common/PeerBadge.tsx @@ -1,18 +1,20 @@ 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 { IconName } from '../../types/icons'; import buildClassName from '../../util/buildClassName'; -import Avatar from './Avatar'; +import Avatar, { type AvatarSize } from './Avatar'; import Icon from './icons/Icon'; import styles from './PeerBadge.module.scss'; type OwnProps = { - peer: ApiPeer | CustomPeer; + peer?: ApiPeer | CustomPeer; + avatarWebPhoto?: ApiWebDocument; + avatarSize?: AvatarSize; text?: string; badgeText?: string; badgeIcon?: IconName; @@ -24,7 +26,9 @@ type OwnProps = { }; const PeerBadge = ({ - peer, + peer: avatarPeer, + avatarWebPhoto, + avatarSize, text, badgeText, badgeIcon, @@ -40,7 +44,7 @@ const PeerBadge = ({ onClick={onClick} >
- + {badgeText && (
{badgeIcon && } diff --git a/src/components/modals/stars/StarsBalanceModal.module.scss b/src/components/modals/stars/StarsBalanceModal.module.scss index b6e7bf997..95d9050a4 100644 --- a/src/components/modals/stars/StarsBalanceModal.module.scss +++ b/src/components/modals/stars/StarsBalanceModal.module.scss @@ -109,6 +109,10 @@ unicode-bidi: plaintext; } +.botItem { + margin-bottom: 0.75rem; +} + .hiddenHeader { transform: translateY(-100%); } @@ -191,15 +195,9 @@ transform: translate(-50%, -50%); } -.paymentAmount { - display: flex; - line-height: 1.125; - gap: 0.125rem; -} - .paymentButton { display: flex; - gap: 0.125rem; + align-items: center; margin-top: 1rem; } @@ -225,3 +223,20 @@ margin-top: 0.5rem; 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; +} diff --git a/src/components/modals/stars/StarsBalanceModal.tsx b/src/components/modals/stars/StarsBalanceModal.tsx index f41efe911..2e74adc61 100644 --- a/src/components/modals/stars/StarsBalanceModal.tsx +++ b/src/components/modals/stars/StarsBalanceModal.tsx @@ -56,7 +56,7 @@ const StarsBalanceModal = ({ modal, starsBalanceState, canBuyPremium, }: OwnProps & StateProps) => { const { - closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingPickerModal, openInvoice, + closeStarsBalanceModal, loadStarsTransactions, loadStarsSubscriptions, openStarsGiftingPickerModal, openInvoice, } = getActions(); const { balance, history, subscriptions } = starsBalanceState || {}; @@ -154,6 +154,10 @@ const StarsBalanceModal = ({ }); }); + const handleLoadMoreSubscriptions = useLastCallback(() => { + loadStarsSubscriptions(); + }); + const openStarsGiftingPickerModalHandler = useLastCallback(() => { openStarsGiftingPickerModal({}); }); @@ -240,6 +244,19 @@ const StarsBalanceModal = ({ subscription={subscription} /> ))} + {subscriptions?.nextOffset && ( + + )}
)} diff --git a/src/components/modals/stars/StarsPaymentModal.tsx b/src/components/modals/stars/StarsPaymentModal.tsx index c6476b122..c97b2e2a2 100644 --- a/src/components/modals/stars/StarsPaymentModal.tsx +++ b/src/components/modals/stars/StarsPaymentModal.tsx @@ -11,6 +11,8 @@ import { selectChat, selectChatMessage, selectUser, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; +import { formatStarsAsIcon } from '../../../util/localization/format'; +import { formatInteger } from '../../../util/textFormat'; import renderText from '../../common/helpers/renderText'; import useFlag from '../../../hooks/useFlag'; @@ -21,6 +23,8 @@ import usePrevious from '../../../hooks/usePrevious'; import Avatar from '../../common/Avatar'; import StarIcon from '../../common/icons/StarIcon'; +import PeerBadge from '../../common/PeerBadge'; +import PickerSelectedItem from '../../common/pickers/PickerSelectedItem'; import SafeLink from '../../common/SafeLink'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; @@ -58,6 +62,8 @@ const StarPaymentModal = ({ const { form, subscriptionInfo } = renderingModal || {}; const amount = form?.invoice?.totalAmount || subscriptionInfo?.subscriptionPricing?.amount; + const isBotSubscription = Boolean(form?.invoice.subscriptionPeriod); + const canShowPeerItem = !subscriptionInfo?.subscriptionPricing; 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]); - }, [renderingModal, bot, oldLang, amount, paidMediaMessage, subscriptionInfo, form, paidMediaChat, lang]); + }, [ + renderingModal?.inputInvoice, bot, oldLang, amount, paidMediaMessage, subscriptionInfo, isBotSubscription, form, + paidMediaChat, lang, + ]); const disclaimerText = useMemo(() => { if (subscriptionInfo) { @@ -160,25 +179,31 @@ const StarPaymentModal = ({ ) : ( - <> - - {photo && } - + )}

{inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}

+ {canShowPeerItem && }
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
{disclaimerText && (
diff --git a/src/components/modals/stars/helpers/transaction.ts b/src/components/modals/stars/helpers/transaction.ts index c16a7862a..0dcddbffc 100644 --- a/src/components/modals/stars/helpers/transaction.ts +++ b/src/components/modals/stars/helpers/transaction.ts @@ -5,7 +5,7 @@ import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/paym export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) { 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.giveawayPostId) return lang('StarsGiveawayPrizeReceived'); if (transaction.isMyGift) return lang('StarsGiftSent'); diff --git a/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss b/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss index aa39c6ec7..bd8ae094c 100644 --- a/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss +++ b/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss @@ -19,6 +19,13 @@ flex-grow: 1; } +.subtitle { + display: flex; + align-items: center; + gap: 0.125rem; + font-size: 0.875rem; +} + .status { display: flex; flex-direction: column; @@ -37,7 +44,7 @@ font-weight: 500; } -.title, .description { +.title, .description, .subtitle { margin-bottom: 0; } diff --git a/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx index 3e80809c0..919e30274 100644 --- a/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx +++ b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx @@ -33,7 +33,7 @@ function selectProvidedPeer(peerId: string) { const StarsSubscriptionItem = ({ subscription }: OwnProps) => { const { openStarsSubscriptionModal } = getActions(); const { - peerId, pricing, until, isCancelled, + peerId, pricing, until, isCancelled, title, photo, } = subscription; const lang = useOldLang(); @@ -57,6 +57,12 @@ const StarsSubscriptionItem = ({ subscription }: OwnProps) => {

{getSenderTitle(lang, peer)}

+ {title && ( +

+ {photo && } + {title} +

+ )}

{lang( hasExpired ? 'StarsSubscriptionExpired' diff --git a/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss b/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss index c3705a040..25b0b9dd4 100644 --- a/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss +++ b/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss @@ -21,6 +21,13 @@ margin-bottom: 0; } +.title { + text-align: center; + text-wrap: balance; + font-size: 1.75rem; + line-height: 1.25; +} + .amount { display: flex; align-items: center; diff --git a/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx b/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx index 472b8113f..82f557e1d 100644 --- a/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx +++ b/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx @@ -8,6 +8,7 @@ import type { import type { TabState } from '../../../../global/types'; import { STARS_ICON_PLACEHOLDER } from '../../../../config'; +import { isApiPeerUser } from '../../../../global/helpers/peers'; import { selectPeer, } from '../../../../global/selectors'; @@ -46,6 +47,7 @@ const StarsSubscriptionModal: FC = ({ changeStarsSubscription, checkChatInvite, loadStarStatus, + openInvoice, } = getActions(); const oldLang = useOldLang(); const lang = useLang(); @@ -69,7 +71,8 @@ const StarsSubscriptionModal: FC = ({ return 'renew'; } - if (!isActive) { + const canRestart = subscription.chatInviteHash || subscription.invoiceSlug; + if (!isActive && canRestart) { return 'restart'; } @@ -87,7 +90,14 @@ const StarsSubscriptionModal: FC = ({ break; } case 'restart': { - checkChatInvite({ hash: subscription.chatInviteHash! }); + if (subscription.chatInviteHash) { + checkChatInvite({ hash: subscription.chatInviteHash }); + } else if (subscription.invoiceSlug) { + openInvoice({ + type: 'slug', + slug: subscription.invoiceSlug, + }); + } loadStarStatus(); break; } @@ -109,13 +119,15 @@ const StarsSubscriptionModal: FC = ({ } const { - pricing, until, isCancelled, canRefulfill, + pricing, until, isCancelled, canRefulfill, photo, title, hasBotCancelled, } = subscription; + const isBotSubscription = isApiPeerUser(peer); + const header = (

- +
= ({ alt="" draggable={false} /> -

{oldLang('StarsSubscriptionTitle')}

+

{title || oldLang('StarsSubscriptionTitle')}

{lang('StarsPerMonth', { amount: pricing.amount, @@ -141,10 +153,17 @@ const StarsSubscriptionModal: FC = ({ const tableData: TableData = []; tableData.push([ - oldLang('StarsSubscriptionChannel'), + oldLang(isBotSubscription ? 'StarsSubscriptionBot' : 'StarsSubscriptionChannel'), { chatId: peer.id }, ]); + if (title) { + tableData.push([ + oldLang('StarsSubscriptionBotProduct'), + title, + ]); + } + const hasExpired = until < Date.now() / 1000; tableData.push([ oldLang(hasExpired ? 'StarsSubscriptionUntilExpired' @@ -162,7 +181,9 @@ const StarsSubscriptionModal: FC = ({

{footerTos}

{isCancelled && ( -

{oldLang('StarsSubscriptionCancelledText')}

+

+ {oldLang(hasBotCancelled ? 'StarsSubscriptionBotCancelledText' : 'StarsSubscriptionCancelledText')} +

)} {canRefulfill && (

diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss index a3e2906b0..5495727e1 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss +++ b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss @@ -37,6 +37,13 @@ margin-bottom: 0; } +.title { + text-align: center; + text-wrap: balance; + font-size: 1.75rem; + line-height: 1.25; +} + .tid { font-family: var(--font-family-monospace); font-size: 0.875rem; diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index 3060eb009..b8f4c3341 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -10,6 +10,7 @@ import { appendStarsSubscriptions, appendStarsTransactions, updateStarsBalance, + updateStarsSubscriptionLoading, } from '../../reducers'; import { selectPeer, @@ -179,6 +180,9 @@ addActionHandler('loadStarsSubscriptions', async (global): Promise => { const offset = subscriptions?.nextOffset; if (subscriptions && !offset) return; // Already loaded all + global = updateStarsSubscriptionLoading(global, true); + setGlobal(global); + const result = await callApi('fetchStarsSubscriptions', { offset: offset || '', }); diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 6ce43be25..86d2f1813 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -42,7 +42,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { case 'updateStarPaymentStateCompleted': { const { paymentState, tabId } = update; - const { inputInvoice, subscriptionInfo } = paymentState; + const { inputInvoice, subscriptionInfo, form } = paymentState; if (inputInvoice?.type === 'chatInviteSubscription' && subscriptionInfo) { 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.userIds) { return; diff --git a/src/global/reducers/payments.ts b/src/global/reducers/payments.ts index bf41dd7b4..10a36bab5 100644 --- a/src/global/reducers/payments.ts +++ b/src/global/reducers/payments.ts @@ -6,7 +6,7 @@ import type { } from '../../api/types'; import type { PaymentStep, ShippingOption } from '../../types'; import type { - GlobalState, StarsTransactionType, TabArgs, TabState, + GlobalState, StarsSubscriptions, StarsTransactionType, TabArgs, TabState, } from '../types'; import { getCurrentTabId } from '../../util/establishMultitabRole'; @@ -182,7 +182,7 @@ export function appendStarsSubscriptions( const newObject = { list: (global.stars.subscriptions?.list || []).concat(subscriptions), nextOffset, - }; + } satisfies StarsSubscriptions; return { ...global, @@ -193,6 +193,26 @@ export function appendStarsSubscriptions( }; } +export function updateStarsSubscriptionLoading( + 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( global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs ): T { diff --git a/src/global/types.ts b/src/global/types.ts index 7fc230b6c..bbb539e42 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -190,6 +190,7 @@ export type StarsTransactionHistory = Record { 'SendPaidReaction': { 'amount': V; }; + 'StarsPay': { + 'amount': V; + }; 'StarsReactionTerms': { 'link': V; }; @@ -1555,6 +1558,9 @@ export interface LangPairWithVariables { 'BotSuggestedStatus': { 'bot': V; }; + 'StarsSubscribeBotButtonMonth': { + 'amount': V; + }; } export interface LangPairPlural { @@ -1731,6 +1737,11 @@ export interface LangPairPluralWithVariables { 'chat': V; 'amount': V; }; + 'StarsSubscribeBotText': { + 'name': V; + 'bot': V; + 'amount': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables; diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index c6288128f..6a4a0dd05 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -76,6 +76,7 @@ const READABLE_ERROR_MESSAGES: Record = { PROVIDER_ACCOUNT_TIMEOUT: 'Request to the payment provider has expired', 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', diff --git a/src/util/localization/format.tsx b/src/util/localization/format.tsx index 7b25ff859..20bf60140 100644 --- a/src/util/localization/format.tsx +++ b/src/util/localization/format.tsx @@ -4,17 +4,21 @@ import type { LangFn } from './types'; import { STARS_ICON_PLACEHOLDER } from '../../config'; +import Icon from '../../components/common/icons/Icon'; import StarIcon from '../../components/common/icons/StarIcon'; export function formatStarsAsText(lang: LangFn, amount: number) { 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 + ? + : ; return lang('StarsAmount', { amount }, { withNodes: true, specialReplacement: { - [STARS_ICON_PLACEHOLDER]: , + [STARS_ICON_PLACEHOLDER]: icon, }, }); }