diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index 8d48ab823..39c39e2a2 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -158,7 +158,7 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId: string): ApiSavedStarGift { const { gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, upgradeStars, transferStars, canUpgrade, - savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt, + savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt, prepaidUpgradeHash, } = userStarGift; const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId @@ -183,6 +183,7 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId canResellAt, canTransferAt, isPinned: pinnedToTop, + prepaidUpgradeHash, }; } diff --git a/src/api/gramjs/apiBuilders/messageActions.ts b/src/api/gramjs/apiBuilders/messageActions.ts index a2d96ff0e..946b1099c 100644 --- a/src/api/gramjs/apiBuilders/messageActions.ts +++ b/src/api/gramjs/apiBuilders/messageActions.ts @@ -390,8 +390,8 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess } if (action instanceof GramJs.MessageActionStarGift) { const { - nameHidden, saved, converted, upgraded, refunded, canUpgrade, gift, message, convertStars, upgradeMsgId, - upgradeStars, fromId, peer, savedId, + nameHidden, saved, converted, upgraded, refunded, canUpgrade, prepaidUpgrade, gift, message, convertStars, + upgradeMsgId, giftMsgId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, } = action; const starGift = buildApiStarGift(gift); @@ -406,20 +406,23 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess isUpgraded: upgraded, isRefunded: refunded, canUpgrade, + isPrepaidUpgrade: prepaidUpgrade, gift: starGift, message: message && buildApiFormattedText(message), starsToConvert: toJSNumber(convertStars), upgradeMsgId, + giftMsgId, alreadyPaidUpgradeStars: toJSNumber(upgradeStars), fromId: fromId && getApiChatIdFromMtpPeer(fromId), peerId: peer && getApiChatIdFromMtpPeer(peer), savedId: savedId !== undefined ? buildApiPeerId(savedId, 'user') : undefined, + prepaidUpgradeHash, }; } if (action instanceof GramJs.MessageActionStarGiftUnique) { const { upgrade, transferred, saved, refunded, gift, canExportAt, transferStars, fromId, peer, savedId, - resaleAmount, + resaleAmount, prepaidUpgrade, } = action; const starGift = buildApiStarGift(gift); @@ -432,6 +435,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess isTransferred: transferred, isSaved: saved, isRefunded: refunded, + isPrepaidUpgrade: prepaidUpgrade, gift: starGift, canExportAt, transferStars: toJSNumber(transferStars), diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 8618e7358..ff8a2937f 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -553,7 +553,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): const { date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction, subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages, - stargiftResale, postsSearch, + stargiftResale, postsSearch, stargiftPrepaidUpgrade, } = transaction; if (photo) { @@ -593,6 +593,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): isGiftResale: stargiftResale, paidMessages, isPostsSearch: postsSearch, + isPrepaidUpgrade: stargiftPrepaidUpgrade, }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index fcf227868..394dca363 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -781,6 +781,13 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { }); } + case 'stargiftPrepaidUpgrade': { + return new GramJs.InputInvoiceStarGiftPrepaidUpgrade({ + peer: buildInputPeer(invoice.peer.id, invoice.peer.accessHash), + hash: invoice.hash, + }); + } + case 'giveaway': default: { const purpose = buildInputStorePaymentPurpose(invoice.purpose); diff --git a/src/api/types/messageActions.ts b/src/api/types/messageActions.ts index 811abd917..42d904260 100644 --- a/src/api/types/messageActions.ts +++ b/src/api/types/messageActions.ts @@ -242,14 +242,17 @@ export interface ApiMessageActionStarGift extends ActionMediaType { isUpgraded?: true; isRefunded?: true; canUpgrade?: true; + isPrepaidUpgrade?: true; gift: ApiStarGiftRegular; message?: ApiFormattedText; starsToConvert?: number; upgradeMsgId?: number; + giftMsgId?: number; alreadyPaidUpgradeStars?: number; fromId?: string; peerId?: string; savedId?: string; + prepaidUpgradeHash?: string; } export interface ApiMessageActionStarGiftUnique extends ActionMediaType { @@ -258,6 +261,7 @@ export interface ApiMessageActionStarGiftUnique extends ActionMediaType { isTransferred?: true; isSaved?: true; isRefunded?: true; + isPrepaidUpgrade?: true; gift: ApiStarGiftUnique; canExportAt?: number; transferStars?: number; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index e76ff4456..7a909ef4c 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -399,10 +399,17 @@ export type ApiInputInvoiceStarGiftTransfer = { recipientId: string; }; +export type ApiInputInvoiceStarGiftPrepaidUpgrade = { + type: 'stargiftPrepaidUpgrade'; + peerId: string; + hash: string; +}; + export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway | ApiInputInvoiceGiftCode | ApiInputInvoicePremiumGiftStars | ApiInputInvoiceStars | ApiInputInvoiceStarsGift | ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription - | ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarGiftTransfer | ApiInputInvoiceStarGiftResale; + | ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarGiftTransfer | ApiInputInvoiceStarGiftResale + | ApiInputInvoiceStarGiftPrepaidUpgrade; /* Used for Invoice request */ export type ApiRequestInputInvoiceMessage = { @@ -472,11 +479,18 @@ export type ApiRequestInputInvoiceStarGiftTransfer = { recipient: ApiPeer; }; +export type ApiRequestInputInvoiceStarGiftPrepaidUpgrade = { + type: 'stargiftPrepaidUpgrade'; + peer: ApiPeer; + hash: string; +}; + export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug | ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway | ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift | ApiRequestInputInvoiceStarGiftUpgrade | ApiRequestInputInvoiceStarGiftTransfer - | ApiRequestInputInvoicePremiumGiftStars | ApiRequestInputInvoiceStarGiftResale; + | ApiRequestInputInvoicePremiumGiftStars | ApiRequestInputInvoiceStarGiftResale + | ApiRequestInputInvoiceStarGiftPrepaidUpgrade; export interface ApiUniqueStarGiftValueInfo { isLastSaleOnFragment?: true; diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index 7a7ee3271..7a9c4ab44 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -107,6 +107,7 @@ export interface ApiSavedStarGift { canTransferAt?: number; canResellAt?: number; isPinned?: boolean; + prepaidUpgradeHash?: string; isConverted?: boolean; // Local field, used for Action Message upgradeMsgId?: number; // Local field, used for Action Message localTag?: number; // Local field, used for key in list @@ -244,6 +245,7 @@ export interface ApiStarsTransaction { isGiftResale?: true; paidMessages?: number; isPostsSearch?: true; + isPrepaidUpgrade?: true; } export interface ApiStarsSubscription { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 1aa5f2806..82dfc85de 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1569,8 +1569,14 @@ "GiftUpgradeTextOwn" = "Turn your gift into a unique collectible that you can transfer or auction."; "GiftUpgradeKeepDetails" = "Keep sender's name and comment"; "GiftUpgradeButton" = "Upgrade for {amount}"; +"GiftPayForUpgradeButton" = "Pay {amount} for Upgrade"; "GiftUpgradedTitle" = "This gift is now a collectible"; "GiftUpgradedDescription" = "You have received a unique number, model, backdrop and symbol for your gift."; +"GiftPeerUpgradeUniqueDescription" = "{user} will get a unique number, model, backdrop, and symbol for the gift."; +"GiftPeerUpgradeTransferableDescription" = "{user} will be able to send the gift to anyone on Telegram."; +"GiftPeerUpgradeTradeableDescription" = "{user} will be able to auction the gift on third-party NFT marketplaces."; +"GiftUpgradeSentTitle" = "Gift Upgrade sent!"; +"GiftUpgradeSentMessage" = "{user} can now upgrade this gift for free."; "GiftMakeUnique" = "Make unique for {stars}"; "GiftMakeUniqueAcc" = "Make unique"; "GiftMakeUniqueDescription" = "Enable this to let {user} turn your gift into a unique collectible. {link}"; @@ -1872,6 +1878,8 @@ "ActionStarGiftReceivedAnonymous" = "Someone sent you a gift for {cost}"; "ActionStarGiftSentChannel" = "{user} sent a gift to {channel} for {cost}"; "ActionStarGiftSentChannelYou" = "You sent a gift to {channel} for {cost}"; +"ActionStarGiftPrepaidUpgradeYou" = "You sent an upgrade worth {cost} for {peer}'s gift"; +"ActionStarGiftPrepaidUpgrade" = "{peer} sent an upgrade worth {cost} for your gift"; "ActionStarGiftSelfBought" = "You bought a gift for {cost}"; "ActionStarGiftTo" = "Gift to {peer}"; "ActionStarGiftFrom" = "Gift from {peer}"; @@ -2293,6 +2301,10 @@ "TitleGiftLocked" = "Gift Locked"; "GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**."; "QuickPreview" = "Quick Preview"; +"GiftAnUpgradeButton" = "Gift an Upgrade"; +"GiftPrepaidUpgradeTransactionTitle" = "Gift Upgrade"; +"ActionStarGiftPrepaidUpgraded" = "{user} turned the gift into a unique collectible"; +"ActionStarGiftPrepaidUpgradedYou" = "You turned the gift into a unique collectible"; "UserNoteTitle" = "Notes"; "UserNoteHint" = "only visible to you"; "EditUserNoteHint" = "Notes are only visible to you."; diff --git a/src/components/middle/message/ActionMessageText.tsx b/src/components/middle/message/ActionMessageText.tsx index 395debe45..f8770963a 100644 --- a/src/components/middle/message/ActionMessageText.tsx +++ b/src/components/middle/message/ActionMessageText.tsx @@ -568,7 +568,7 @@ const ActionMessageText = ({ case 'starGift': { const { - gift, alreadyPaidUpgradeStars, peerId, savedId, fromId, + gift, alreadyPaidUpgradeStars, peerId, savedId, fromId, isPrepaidUpgrade, } = action; const isToChannel = Boolean(peerId && savedId); @@ -576,9 +576,25 @@ const ActionMessageText = ({ const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText; const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview); + const toPeer = peerId ? selectPeer(global, peerId) : undefined; + const toTitle = (toPeer && getPeerTitle(lang, toPeer)) + || (isToChannel ? channelFallbackText : userFallbackText); + const toLink = renderPeerLink(toPeer?.id, toTitle, asPreview); + const starsAmount = gift.stars + (alreadyPaidUpgradeStars || 0); const cost = renderStrong(formatStarsAsText(lang, starsAmount)); + if (isPrepaidUpgrade && gift.upgradeStars) { + const upgradeCost = renderStrong(formatStarsAsText(lang, gift.upgradeStars)); + + return translateWithYou( + lang, 'ActionStarGiftPrepaidUpgrade', isOutgoing, { + peer: isOutgoing ? toLink : senderLink, + cost: upgradeCost, + }, + ); + } + if (isToChannel) { const channelPeer = selectPeer(global, peerId!); const isYou = fromPeer?.id === currentUserId; @@ -607,7 +623,7 @@ const ActionMessageText = ({ case 'starGiftUnique': { const { - isTransferred, isUpgrade, savedId, peerId, fromId, resaleAmount, gift, transferStars, + isTransferred, isUpgrade, savedId, peerId, fromId, resaleAmount, gift, transferStars, isPrepaidUpgrade, } = action; const isToChannel = Boolean(peerId && savedId); @@ -616,6 +632,18 @@ const ActionMessageText = ({ const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText; const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview); + const toPeer = peerId ? selectPeer(global, peerId) : undefined; + const toTitle = (toPeer && getPeerTitle(lang, toPeer)) + || (isToChannel ? channelFallbackText : userFallbackText); + const toLink = renderPeerLink(toPeer?.id, toTitle, asPreview); + + if (isPrepaidUpgrade) { + if (isOutgoing) { + return lang('ActionStarGiftPrepaidUpgradedYou'); + } + return lang('ActionStarGiftPrepaidUpgraded', { user: toLink }, { withNodes: true }); + } + if (resaleAmount && !transferStars) { const amountText = resaleAmount.currency === TON_CURRENCY_CODE ? formatTonAsText(lang, convertTonFromNanos(resaleAmount.amount)) diff --git a/src/components/modals/gift/UniqueGiftHeader.module.scss b/src/components/modals/gift/UniqueGiftHeader.module.scss index 8c8d29a3f..4e6210eb1 100644 --- a/src/components/modals/gift/UniqueGiftHeader.module.scss +++ b/src/components/modals/gift/UniqueGiftHeader.module.scss @@ -93,7 +93,7 @@ } .subtitle { - max-width: 75%; + max-width: 85%; font-size: 0.875rem; text-align: center; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 076fce12e..b2b095d36 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -282,7 +282,8 @@ const GiftInfoModal = ({ const handleOpenUpgradeModal = useLastCallback(() => { if (!savedGift) return; - openGiftUpgradeModal({ giftId: savedGift.gift.id, gift: savedGift }); + const giftOwnerId = renderingTargetPeer?.id; + openGiftUpgradeModal({ giftId: savedGift.gift.id, gift: savedGift, peerId: giftOwnerId }); }); const handleBuyGift = useLastCallback(() => { @@ -376,6 +377,15 @@ const GiftInfoModal = ({ ); } + if (savedGift?.prepaidUpgradeHash) { + return ( + + ); + } + return ( )} @@ -160,7 +194,9 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { header, footer, }; - }, [previewAttributes, isOpen, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails]); + }, [previewAttributes, isOpen, lang, + renderingRecipient, renderingModal?.gift, + shouldKeepOriginalDetails, isPrepaid]); return ( payInputStarInvoice(global, invoice, transferStars, tabId); }); +addActionHandler('upgradePrepaidGift', (global, actions, payload): ActionReturnType => { + const { peerId, hash, stars, tabId = getCurrentTabId() } = payload; + + const invoice: ApiInputInvoice = { + type: 'stargiftPrepaidUpgrade', + peerId, + hash, + }; + + payInputStarInvoice(global, invoice, stars, tabId); +}); + async function payInputStarInvoice( global: T, inputInvoice: ApiInputInvoice, price: number, ...[tabId = getCurrentTabId()]: TabArgs diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index f6d4375da..4faf277ba 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -2,10 +2,11 @@ import type { ActionReturnType } from '../../types'; import { formatCurrencyAsString } from '../../../util/formatCurrency'; import * as langProvider from '../../../util/oldLangProvider'; +import { getPeerTitle } from '../../helpers/peers'; import { addActionHandler, setGlobal } from '../../index'; import { updateStarsBalance } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; -import { selectTabState } from '../../selectors'; +import { selectPeer, selectTabState } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { switch (update['@type']) { @@ -175,6 +176,24 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { actions.reloadPeerSavedGifts({ peerId: global.currentUserId }); } + if (inputInvoice?.type === 'stargiftPrepaidUpgrade') { + actions.reloadPeerSavedGifts({ peerId: inputInvoice.peerId }); + + const lang = langProvider.getTranslationFn(); + const peer = selectPeer(global, inputInvoice.peerId); + const peerTitle = peer ? getPeerTitle(lang, peer) : undefined; + + actions.showNotification({ + icon: 'gift', + title: { key: 'GiftUpgradeSentTitle' }, + message: { + key: 'GiftUpgradeSentMessage', + variables: { user: peerTitle }, + }, + tabId, + }); + } + break; } diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 40d956c71..d6dc77983 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -233,12 +233,19 @@ addActionHandler('openGiftInfoModalFromMessage', async (global, actions, payload const starGift = action.type === 'starGift' ? action : undefined; const uniqueGift = action.type === 'starGiftUnique' ? action : undefined; + const giftMsgId = starGift?.giftMsgId; const giftReceiverId = action.peerId || (message.isOutgoing ? message.chatId : global.currentUserId!); - const inputGift: ApiInputSavedStarGift = action.savedId - ? { type: 'chat', chatId, savedId: action.savedId } - : { type: 'user', messageId }; + const inputGift: ApiInputSavedStarGift = (() => { + if (giftMsgId) { + return { type: 'user', messageId: giftMsgId }; + } + if (action.savedId) { + return { type: 'chat', chatId, savedId: action.savedId }; + } + return { type: 'user', messageId }; + })(); const fromId = action.fromId || (message.isOutgoing ? global.currentUserId! : message.chatId); @@ -259,6 +266,7 @@ addActionHandler('openGiftInfoModalFromMessage', async (global, actions, payload canExportAt: uniqueGift?.canExportAt, savedId: action.savedId, transferStars: uniqueGift?.transferStars, + prepaidUpgradeHash: starGift?.prepaidUpgradeHash, }; actions.openGiftInfoModal({ peerId: giftReceiverId, gift, tabId }); diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 3e5a31c3f..1a34ceab1 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -235,6 +235,18 @@ export function getRequestInputInvoice( }; } + if (inputInvoice.type === 'stargiftPrepaidUpgrade') { + const { peerId, hash } = inputInvoice; + const peer = selectPeer(global, peerId); + if (!peer) return undefined; + + return { + type: 'stargiftPrepaidUpgrade', + peer, + hash, + }; + } + return undefined; } diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 173291bd9..8a1d11883 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -2629,6 +2629,11 @@ export interface ActionPayloads { shouldKeepOriginalDetails?: boolean; upgradeStars?: number; } & WithTabId; + upgradePrepaidGift: { + peerId: string; + hash: string; + stars: number; + } & WithTabId; openGiftWithdrawModal: { gift: ApiSavedStarGift; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 7d074947b..8cdfc45e2 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1303,6 +1303,7 @@ export interface LangPair { 'GiftUpgradeKeepDetails': undefined; 'GiftUpgradedTitle': undefined; 'GiftUpgradedDescription': undefined; + 'GiftUpgradeSentTitle': undefined; 'GiftMakeUniqueAcc': undefined; 'GiftMakeUniqueLink': undefined; 'GiftWithdrawTitle': undefined; @@ -1716,6 +1717,9 @@ export interface LangPair { 'ConfirmBuyGiftForTonDescription': undefined; 'TitleGiftLocked': undefined; 'QuickPreview': undefined; + 'GiftAnUpgradeButton': undefined; + 'GiftPrepaidUpgradeTransactionTitle': undefined; + 'ActionStarGiftPrepaidUpgradedYou': undefined; 'UserNoteTitle': undefined; 'UserNoteHint': undefined; 'EditUserNoteHint': undefined; @@ -2193,6 +2197,21 @@ export interface LangPairWithVariables { 'GiftUpgradeButton': { 'amount': V; }; + 'GiftPayForUpgradeButton': { + 'amount': V; + }; + 'GiftPeerUpgradeUniqueDescription': { + 'user': V; + }; + 'GiftPeerUpgradeTransferableDescription': { + 'user': V; + }; + 'GiftPeerUpgradeTradeableDescription': { + 'user': V; + }; + 'GiftUpgradeSentMessage': { + 'user': V; + }; 'GiftMakeUnique': { 'stars': V; }; @@ -2486,6 +2505,14 @@ export interface LangPairWithVariables { 'channel': V; 'cost': V; }; + 'ActionStarGiftPrepaidUpgradeYou': { + 'cost': V; + 'peer': V; + }; + 'ActionStarGiftPrepaidUpgrade': { + 'peer': V; + 'cost': V; + }; 'ActionStarGiftSelfBought': { 'cost': V; }; @@ -2945,6 +2972,9 @@ export interface LangPairWithVariables { 'GiftLockedMessage': { 'relativeDate': V; }; + 'ActionStarGiftPrepaidUpgraded': { + 'user': V; + }; } export interface LangPairPlural {