diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 158ad2a0c..88565e2e3 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -12,6 +12,10 @@ import { TODO_ITEM_LENGTH_LIMIT, TODO_ITEMS_LIMIT, TODO_TITLE_LENGTH_LIMIT, + TON_SUGGESTED_POST_AMOUNT_MAX, + TON_SUGGESTED_POST_AMOUNT_MIN, + TON_TOPUP_URL_DEFAULT, + TON_USD_RATE_DEFAULT, } from '../../../config'; import localDb from '../localDb'; import { buildJson } from './misc'; @@ -108,6 +112,10 @@ export interface GramJsAppConfig extends LimitsConfig { stars_suggested_post_future_max?: number; stars_suggested_post_future_min?: number; ton_suggested_post_commission_permille?: number; + ton_suggested_post_amount_max?: number; + ton_suggested_post_amount_min?: number; + ton_usd_rate?: number; + ton_topup_url?: string; poll_answers_max?: number; todo_items_max?: number; todo_title_length_max?: number; @@ -219,6 +227,10 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp starsSuggestedPostFutureMax: appConfig.stars_suggested_post_future_max, starsSuggestedPostFutureMin: appConfig.stars_suggested_post_future_min, tonSuggestedPostCommissionPermille: appConfig.ton_suggested_post_commission_permille, + tonSuggestedPostAmountMax: appConfig.ton_suggested_post_amount_max ?? TON_SUGGESTED_POST_AMOUNT_MAX, + tonSuggestedPostAmountMin: appConfig.ton_suggested_post_amount_min ?? TON_SUGGESTED_POST_AMOUNT_MIN, + tonUsdRate: appConfig.ton_usd_rate ?? TON_USD_RATE_DEFAULT, + tonTopupUrl: appConfig.ton_topup_url ?? TON_TOPUP_URL_DEFAULT, pollMaxAnswers: appConfig.poll_answers_max, todoItemsMax: appConfig.todo_items_max ?? TODO_ITEMS_LIMIT, todoTitleLengthMax: appConfig.todo_title_length_max ?? TODO_TITLE_LENGTH_LIMIT, diff --git a/src/api/gramjs/apiBuilders/messageActions.ts b/src/api/gramjs/apiBuilders/messageActions.ts index d91fc535c..5dc75586e 100644 --- a/src/api/gramjs/apiBuilders/messageActions.ts +++ b/src/api/gramjs/apiBuilders/messageActions.ts @@ -7,7 +7,7 @@ import { buildApiBotApp } from './bots'; import { buildApiFormattedText, buildApiPhoto } from './common'; import { buildApiStarGift } from './gifts'; import { buildTodoItem } from './messageContent'; -import { buildApiStarsAmount } from './payments'; +import { buildApiCurrencyAmount } from './payments'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; const UNSUPPORTED_ACTION: ApiMessageAction = { @@ -359,6 +359,20 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess transactionId, }; } + if (action instanceof GramJs.MessageActionGiftTon) { + const { + currency, amount, cryptoCurrency, cryptoAmount, transactionId, + } = action; + return { + mediaType: 'action', + type: 'giftTon', + currency, + amount: amount.toJSNumber(), + cryptoCurrency, + cryptoAmount: cryptoAmount.toJSNumber(), + transactionId, + }; + } if (action instanceof GramJs.MessageActionPrizeStars) { const { unclaimed, stars, transactionId, boostPeer, giveawayMsgId, @@ -459,7 +473,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess isBalanceTooLow: Boolean(balanceTooLow), rejectComment, scheduleDate, - amount: price ? buildApiStarsAmount(price) : undefined, + amount: price ? buildApiCurrencyAmount(price) : undefined, }; } if (action instanceof GramJs.MessageActionSuggestedPostSuccess) { @@ -467,7 +481,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess return { mediaType: 'action', type: 'suggestedPostSuccess', - amount: buildApiStarsAmount(price), + amount: buildApiCurrencyAmount(price), }; } if (action instanceof GramJs.MessageActionSuggestedPostRefund) { diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index a3f38a73e..8c55f4d76 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -47,7 +47,7 @@ import { omitUndefined, pick } from '../../../util/iteratees'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { interpolateArray } from '../../../util/waveform'; import { - buildApiStarsAmount, + buildApiCurrencyAmount, } from '../apiBuilders/payments'; import { buildPeer } from '../gramjsBuilders'; import { @@ -302,7 +302,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un const suggestedPostInfo = suggestedPost instanceof GramJs.SuggestedPost ? { isAccepted: suggestedPost.accepted, isRejected: suggestedPost.rejected, - price: suggestedPost.price ? buildApiStarsAmount(suggestedPost.price) : undefined, + price: suggestedPost.price ? buildApiCurrencyAmount(suggestedPost.price) : undefined, scheduleDate: suggestedPost.scheduleDate, } satisfies ApiInputSuggestedPostInfo : undefined; @@ -319,7 +319,7 @@ function buildApiSuggestedPost(suggestedPost: GramJs.SuggestedPost): ApiSuggeste return { isAccepted: suggestedPost.accepted, isRejected: suggestedPost.rejected, - price: suggestedPost.price ? buildApiStarsAmount(suggestedPost.price) : undefined, + price: suggestedPost.price ? buildApiCurrencyAmount(suggestedPost.price) : undefined, scheduleDate: suggestedPost.scheduleDate, }; } diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index fb578a2ad..85f42abe6 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -20,15 +20,16 @@ import type { ApiPrepaidStarsGiveaway, ApiReceipt, ApiStarGiveawayOption, - ApiStarsAmount, ApiStarsGiveawayWinnerOption, ApiStarsSubscription, ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarTopupOption, + ApiTypeCurrencyAmount, BoughtPaidMedia, } from '../../types'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config'; import { addWebDocumentToLocalDb } from '../helpers/localDb'; import { buildApiStarsSubscriptionPricing } from './chats'; import { buildApiMessageEntity } from './common'; @@ -461,28 +462,25 @@ export function buildApiStarsGiftOptions(option: GramJs.StarsGiftOption): ApiSta }; } -export function buildApiStarsAmount(amount: GramJs.TypeStarsAmount): ApiStarsAmount | undefined { +export function buildApiCurrencyAmount(amount: GramJs.TypeStarsAmount): ApiTypeCurrencyAmount | undefined { if (amount instanceof GramJs.StarsAmount) { return { + currency: STARS_CURRENCY_CODE, amount: amount.amount.toJSNumber(), nanos: amount.nanos, }; } if (amount instanceof GramJs.StarsTonAmount) { - return undefined; + return { + currency: TON_CURRENCY_CODE, + amount: amount.amount.toJSNumber(), + }; } return undefined; } -export function buildInputStarsAmount(amount: ApiStarsAmount): GramJs.TypeStarsAmount { - return new GramJs.StarsAmount({ - amount: bigInt(amount.amount), - nanos: amount.nanos, - }); -} - export function buildApiStarsGiveawayWinnersOption( option: GramJs.StarsGiveawayWinnersOption, ): ApiStarsGiveawayWinnerOption { @@ -563,7 +561,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): const starRefCommision = starrefCommissionPermille ? starrefCommissionPermille / 10 : undefined; - const starsAmount = buildApiStarsAmount(amount); + const starsAmount = buildApiCurrencyAmount(amount); if (!starsAmount) { return undefined; } @@ -572,7 +570,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): id, date, peer: buildApiStarsTransactionPeer(peer), - stars: starsAmount, + amount: starsAmount, title, description, photo: photo && buildApiWebDocument(photo), diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index e8e2900b0..92d4a6745 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -33,15 +33,15 @@ import type { ApiStory, ApiStorySkipped, ApiThemeParameters, + ApiTypeCurrencyAmount, ApiVideo, } from '../../types'; import { ApiMessageEntityTypes, } from '../../types'; -import { CHANNEL_ID_BASE, DEFAULT_STATUS_ICON_ID } from '../../../config'; +import { CHANNEL_ID_BASE, DEFAULT_STATUS_ICON_ID, STARS_CURRENCY_CODE } from '../../../config'; import { pick } from '../../../util/iteratees'; -import { buildInputStarsAmount } from '../apiBuilders/payments'; import { deserializeBytes } from '../helpers/misc'; import localDb from '../localDb'; @@ -890,12 +890,22 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) { return undefined; } -export function buildInputSuggestedPost(suggestedPostInfo: ApiInputSuggestedPostInfo): GramJs.SuggestedPost { - const isPaid = Boolean(suggestedPostInfo.price) - && Boolean((suggestedPostInfo.price.amount || suggestedPostInfo.price.nanos)); +export function buildInputStarsAmount(amount: ApiTypeCurrencyAmount): GramJs.TypeStarsAmount { + if (amount.currency === STARS_CURRENCY_CODE) { + return new GramJs.StarsAmount({ + amount: BigInt(amount.amount), + nanos: amount.nanos, + }); + } + return new GramJs.StarsTonAmount({ + amount: BigInt(amount.amount), + }); +} + +export function buildInputSuggestedPost(suggestedPostInfo: ApiInputSuggestedPostInfo): GramJs.SuggestedPost { return new GramJs.SuggestedPost({ - price: isPaid ? buildInputStarsAmount(suggestedPostInfo.price!) : undefined, + price: suggestedPostInfo.price && buildInputStarsAmount(suggestedPostInfo.price), scheduleDate: suggestedPostInfo.scheduleDate, }); } diff --git a/src/api/gramjs/methods/stars.ts b/src/api/gramjs/methods/stars.ts index d58a279a4..8eb04d461 100644 --- a/src/api/gramjs/methods/stars.ts +++ b/src/api/gramjs/methods/stars.ts @@ -14,7 +14,7 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift, buildApiStarGiftAttribute, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts'; import { - buildApiStarsAmount, + buildApiCurrencyAmount, buildApiStarsGiftOptions, buildApiStarsGiveawayOptions, buildApiStarsSubscription, @@ -182,18 +182,22 @@ export async function getStarsGiftOptions({ return result.map(buildApiStarsGiftOptions); } -export async function fetchStarsStatus() { +export async function fetchStarsStatus({ + isTon, +}: { + isTon?: boolean; +} = {}) { const result = await invokeRequest(new GramJs.payments.GetStarsStatus({ peer: new GramJs.InputPeerSelf(), + ton: isTon || undefined, })); if (!result) { return undefined; } - const balance = buildApiStarsAmount(result.balance); + const balance = buildApiCurrencyAmount(result.balance); if (!balance) { - // For now, skip if balance is in TON return undefined; } @@ -212,29 +216,31 @@ export async function fetchStarsTransactions({ limit = DEFAULT_PRIMITIVES.INT, isInbound, isOutbound, + isTon, }: { peer?: ApiPeer; offset?: string; limit?: number; - isInbound?: true; - isOutbound?: true; + isInbound?: boolean; + isOutbound?: boolean; + isTon?: boolean; }) { const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(); const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({ peer: inputPeer, offset, limit, - inbound: isInbound, - outbound: isOutbound, + inbound: isInbound || undefined, + outbound: isOutbound || undefined, + ton: isTon || undefined, })); if (!result) { return undefined; } - const balance = buildApiStarsAmount(result.balance); + const balance = buildApiCurrencyAmount(result.balance); if (!balance) { - // For now, skip if balance is in TON return undefined; } @@ -246,14 +252,16 @@ export async function fetchStarsTransactions({ } export async function fetchStarsTransactionById({ - id, peer, + id, peer, ton, }: { id: string; peer?: ApiPeer; + ton?: true; }) { const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(); const result = await invokeRequest(new GramJs.payments.GetStarsTransactionsByID({ peer: inputPeer, + ton, id: [new GramJs.InputStarsTransaction({ id, })], @@ -285,9 +293,8 @@ export async function fetchStarsSubscriptions({ return undefined; } - const balance = buildApiStarsAmount(result.balance); + const balance = buildApiCurrencyAmount(result.balance); if (!balance) { - // For now, skip if balance is in TON return undefined; } diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index 142ff8a0e..44bad14ca 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -272,6 +272,22 @@ export async function fetchPremiumGifts() { }; } +export async function fetchTonGifts() { + const result = await invokeRequest(new GramJs.messages.GetStickerSet({ + stickerset: new GramJs.InputStickerSetTonGifts(), + hash: DEFAULT_PRIMITIVES.INT, + })); + + if (!(result instanceof GramJs.messages.StickerSet)) { + return undefined; + } + + return { + set: buildStickerSet(result.set), + stickers: processStickerResult(result.documents), + }; +} + export async function fetchDefaultTopicIcons() { const result = await invokeRequest(new GramJs.messages.GetStickerSet({ stickerset: new GramJs.InputStickerSetEmojiDefaultTopicIcons(), diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index bcf523f80..b8d1bcf74 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -50,7 +50,7 @@ import { buildLangStrings, buildPrivacyKey, } from '../apiBuilders/misc'; -import { buildApiStarsAmount } from '../apiBuilders/payments'; +import { buildApiCurrencyAmount } from '../apiBuilders/payments'; import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPaidReactionPrivacy, @@ -1042,7 +1042,7 @@ export function updater(update: Update) { isEnabled: update.enabled ? true : undefined, }); } else if (update instanceof GramJs.UpdateStarsBalance) { - const balance = buildApiStarsAmount(update.balance); + const balance = buildApiCurrencyAmount(update.balance); if (!balance) { // Skip TON balance updates for now return; diff --git a/src/api/types/messageActions.ts b/src/api/types/messageActions.ts index 76806c2a7..333c66242 100644 --- a/src/api/types/messageActions.ts +++ b/src/api/types/messageActions.ts @@ -1,7 +1,7 @@ import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls'; import type { ApiBotApp, ApiFormattedText, ApiPhoto } from './messages'; import type { ApiTodoItem } from './messages'; -import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiStarsAmount } from './stars'; +import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars'; interface ActionMediaType { mediaType: 'action'; @@ -216,6 +216,15 @@ export interface ApiMessageActionGiftStars extends ActionMediaType { transactionId?: string; } +export interface ApiMessageActionGiftTon extends ActionMediaType { + type: 'giftTon'; + currency: string; + amount: number; + cryptoCurrency: string; + cryptoAmount: number; + transactionId?: string; +} + export interface ApiMessageActionPrizeStars extends ActionMediaType { type: 'prizeStars'; isUnclaimed?: true; @@ -288,12 +297,12 @@ export interface ApiMessageActionSuggestedPostApproval extends ActionMediaType { isBalanceTooLow?: boolean; rejectComment?: string; scheduleDate?: number; - amount?: ApiStarsAmount; + amount?: ApiTypeCurrencyAmount; } export interface ApiMessageActionSuggestedPostSuccess extends ActionMediaType { type: 'suggestedPostSuccess'; - amount?: ApiStarsAmount; + amount?: ApiTypeCurrencyAmount; } export interface ApiMessageActionSuggestedPostRefund extends ActionMediaType { @@ -328,7 +337,7 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha | ApiMessageActionTopicCreate | ApiMessageActionTopicEdit | ApiMessageActionSuggestProfilePhoto | ApiMessageActionChannelJoined | ApiMessageActionGiftCode | ApiMessageActionGiveawayLaunch | ApiMessageActionGiveawayResults | ApiMessageActionPaymentRefunded | ApiMessageActionGiftStars - | ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique + | ApiMessageActionGiftTon | ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique | ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval | ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions | ApiMessageActionTodoAppendTasks; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index c675b901d..c1bb4e27c 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -9,7 +9,7 @@ import type { ApiMessageAction } from './messageActions'; import type { ApiLabeledPrice, } from './payments'; -import type { ApiStarGiftUnique, ApiStarsAmount } from './stars'; +import type { ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars'; import type { ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData, } from './stories'; @@ -415,7 +415,7 @@ export interface ApiInputMessageReplyInfo { export interface ApiSuggestedPost { isAccepted?: true; isRejected?: true; - price?: ApiStarsAmount; + price?: ApiTypeCurrencyAmount; scheduleDate?: number; } @@ -426,7 +426,7 @@ export interface ApiInputStoryReplyInfo { } export interface ApiInputSuggestedPostInfo { - price?: ApiStarsAmount; + price?: ApiTypeCurrencyAmount; scheduleDate?: number; isAccepted?: true; isRejected?: true; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 502c60ee2..79d2dac97 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -259,6 +259,10 @@ export interface ApiAppConfig { starsSuggestedPostFutureMax?: number; starsSuggestedPostFutureMin?: number; tonSuggestedPostCommissionPermille?: number; + tonSuggestedPostAmountMax?: number; + tonSuggestedPostAmountMin?: number; + tonUsdRate?: number; + tonTopupUrl?: string; pollMaxAnswers?: number; todoItemsMax?: number; todoTitleLengthMax?: number; diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index 72e7571bc..67dfeb073 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -1,3 +1,4 @@ +import type { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../config'; import type { ApiWebDocument } from './bots'; import type { ApiChat } from './chats'; import type { ApiFormattedText, ApiSticker, BoughtPaidMedia } from './messages'; @@ -153,11 +154,19 @@ export type ApiRequestInputSavedStarGiftChat = { }; export type ApiRequestInputSavedStarGift = ApiRequestInputSavedStarGiftUser | ApiRequestInputSavedStarGiftChat; +export type ApiTypeCurrencyAmount = ApiStarsAmount | ApiTonAmount; + export interface ApiStarsAmount { + currency: typeof STARS_CURRENCY_CODE; amount: number; nanos: number; } +export interface ApiTonAmount { + currency: typeof TON_CURRENCY_CODE; + amount: number; +} + export interface ApiStarsTransactionPeerUnsupported { type: 'unsupported'; } @@ -205,7 +214,7 @@ export interface ApiStarsTransaction { id?: string; peer: ApiStarsTransactionPeer; messageId?: number; - stars: ApiStarsAmount; + amount: ApiTypeCurrencyAmount; isRefund?: true; isGift?: true; starGift?: ApiStarGift; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index b2be53069..9f23cca97 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -43,7 +43,7 @@ import type { ApiSessionData, } from './misc'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; -import type { ApiStarsAmount } from './stars'; +import type { ApiTypeCurrencyAmount } from './stars'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { ApiEmojiStatusType, ApiUser, ApiUserFullInfo, ApiUserStatus, @@ -794,7 +794,7 @@ export type ApiUpdatePremiumFloodWait = { export type ApiUpdateStarsBalance = { '@type': 'updateStarsBalance'; - balance: ApiStarsAmount; + balance: ApiTypeCurrencyAmount; }; export type ApiUpdateDeleteProfilePhoto = { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index fca2cc1e7..55e687743 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -657,6 +657,7 @@ "MenuStickers" = "Stickers and Emoji"; "MenuAnimations" = "Animations and Performance"; "MenuStars" = "My Stars"; +"MenuTon" = "My TON"; "MenuSendGift" = "Send a Gift"; "MenuTelegramFaq" = "Telegram FAQ"; "MenuPrivacyPolicy" = "Privacy Policy"; @@ -1880,6 +1881,10 @@ "ActionGiftStarsTitle_one" = "{amount} Star"; "ActionGiftStarsTitle_other" = "{amount} Stars"; "ActionGiftStarsText" = "Use Stars to unlock content and services on Telegram."; +"TonAmount" = "💎{amount}"; +"TonAmountText_one" = "{amount} TON"; +"TonAmountText_other" = "{amount} TON"; +"ActionGiftCostCrypto" = "{cryptoPrice} ({price})"; "ActionBoostApplyYou_one" = "You boosted the group"; "ActionBoostApplyYou_other" = "You boosted the group {count} times"; "ActionBoostApply_one" = "{from} boosted the group"; @@ -2040,11 +2045,14 @@ "TitleTime" = "Time"; "TitleSuggestMessage" = "Suggest a Message"; "TitleSuggestedChanges" = "Suggest Changes"; -"EnterPriceInStars" = "Enter Price In Stars"; -"SuggestMessagePriceDescription" = "Choose how many {currency} to pay to publish this post."; +"SuggestMessageNoPrice" = "Free"; +"EnterPriceInStars" = "Enter Price in Stars"; +"EnterPriceInTon" = "Enter Price in TON"; +"SuggestMessagePriceDescriptionStars" = "Choose how many Stars to pay to publish this post."; +"SuggestMessagePriceDescriptionTon" = "Choose how many TON to pay to publish this post."; "SuggestMessageDateTimeHint" = "Select the date and time you want the post to be published."; "SuggestMessageTimeDescription" = "{hint} The post will remain available for at least {duration} from this date."; -"TitleAnytime" = "Anytime"; +"SuggestMessageAnytime" = "Anytime"; "ButtonOfferAmount" = "Offer {amount}"; "ButtonOfferFree" = "Offer Free"; "ButtonUpdateTerms" = "Update Terms"; @@ -2070,6 +2078,7 @@ "SuggestedPostRefundedByUser" = "{channel} will not receive {amount} because {user} requested a refund."; "SuggestedPostRefundedByChannel" = "{amount} was returned to {peer} because {channel} deleted the message."; "CurrencyStars" = "Stars"; +"CurrencyTon" = "TON"; "DeclineReasonPlaceholder" = "Add a reason (optional)"; "DeclinePostDialogQuestion" = "Do you want to decline this post from **{sender}**?"; "SuggestedPostRejected" = "**{peer}** rejected this message."; @@ -2126,4 +2135,9 @@ "ToDoListErrorChooseTitle" = "Please enter a title."; "ToDoListErrorChooseTasks" = "Please enter at least one task."; "GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**"; -"PremiumPreviewTodo" = "Checklists"; \ No newline at end of file +"PremiumPreviewTodo" = "Checklists"; +"MenuTon" = "My TON"; +"DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram."; +"ButtonTopUpViaFragment" = "Top-Up Via Fragment"; +"TonModalHint" = "You can top-up your TON using Fragment."; +"TonGiftReceived" = "TON Top-Up"; diff --git a/src/assets/tgs/Diamond.tgs b/src/assets/tgs/Diamond.tgs new file mode 100644 index 000000000..63d1896d9 Binary files /dev/null and b/src/assets/tgs/Diamond.tgs differ diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index b67221519..56d1db774 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -156,11 +156,19 @@ font-weight: var(--font-weight-semibold) !important; line-height: 1; - animation: hide-icon 0.4s forwards ease-out; &.visible { animation: grow-icon 0.4s ease-out; } + &.hiding { + animation: hide-icon 0.4s forwards ease-out; + } + + &.hidden { + pointer-events: none; + opacity: 0; + } + .icon { font-size: 0.875rem; } diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 0587bb412..c9e11a20f 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -55,6 +55,7 @@ import { SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL, SERVICE_NOTIFICATIONS_USER_ID, + STARS_CURRENCY_CODE, } from '../../config'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { @@ -142,6 +143,7 @@ import useGetSelectionRange from '../../hooks/useGetSelectionRange'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; +import usePrevious from '../../hooks/usePrevious'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import useSchedule from '../../hooks/useSchedule'; import useSendMessageAction from '../../hooks/useSendMessageAction'; @@ -1576,7 +1578,7 @@ const Composer: FC = ({ }); const handleSuggestPostClick = useLastCallback(() => { updateDraftSuggestedPostInfo({ - price: { amount: 0, nanos: 0 }, + price: { currency: STARS_CURRENCY_CODE, amount: 0, nanos: 0 }, }); }); @@ -1874,6 +1876,7 @@ const Composer: FC = ({ const effectEmoji = areEffectsSupported && effect?.emoticon; const shouldRenderPaidBadge = Boolean(paidMessagesStars && mainButtonState === MainButtonState.Send); + const prevShouldRenderPaidBadge = usePrevious(shouldRenderPaidBadge); return (
@@ -2358,7 +2361,12 @@ const Composer: FC = ({ {isInMessageList && } {isInMessageList && } + )} + {canBuyPremium && !areBuyOptionsShown && shouldSuggestGifting && ( + + )} + {areBuyOptionsShown && starsBalanceState?.topupOptions && ( + + )} + + ); + }; + + const renderTonSection = () => { + const tonAmount = convertCurrencyFromBaseUnit(balance?.amount || 0, TON_CURRENCY_CODE); + return ( + <> + +

+ {lang('CurrencyTon')} +

+
+ {lang('DescriptionAboutTon')} +
+
+
+ + {tonAmount} +
+ {Boolean(tonUsdRate) && ( + + {`≈ ${formatCurrencyAsString( + convertTonToUsd(balance?.amount || 0, tonUsdRate), + 'USD', + lang.code, + )}`} + + )} +
+ + + ); + }; + function handleScroll(e: React.UIEvent) { const { scrollTop } = e.currentTarget; @@ -168,6 +268,7 @@ const StarsBalanceModal = ({ const handleLoadMoreTransactions = useLastCallback(() => { loadStarsTransactions({ type: TRANSACTION_TYPES[selectedTabIndex], + isTon: currency === TON_CURRENCY_CODE, }); }); @@ -188,6 +289,10 @@ const StarsBalanceModal = ({ }); }); + const handleTonTopUp = useLastCallback(() => { + openUrl({ url: tonTopupUrl }); + }); + return ( - + {currency !== TON_CURRENCY_CODE && }

{oldLang('TelegramStars')}

- - -

- {starsNeeded ? oldLang('StarsNeededTitle', ongoingTransactionAmount) : oldLang('TelegramStars')} -

-
- {renderText( - starsNeededText || oldLang('TelegramStarsInfo'), - ['simple_markdown', 'emoji'], - )} -
- {canBuyPremium && !areBuyOptionsShown && ( - - )} - {canBuyPremium && !areBuyOptionsShown && shouldSuggestGifting && ( - - )} - {areBuyOptionsShown && starsBalanceState?.topupOptions && ( - - )} + {currency === TON_CURRENCY_CODE ? renderTonSection() : renderStarsSection()}
{areBuyOptionsShown && (
{tosText}
)} + {currency === TON_CURRENCY_CODE && ( +
+ {lang('TonModalHint')} +
+ )} {shouldShowItems && Boolean(subscriptions?.list.length) && (

{oldLang('StarMySubscriptions')}

@@ -325,13 +400,18 @@ const StarsBalanceModal = ({ }; export default memo(withGlobal( - (global): StateProps => { - const shouldForceHeight = Boolean(global.stars?.history?.all?.transactions.length); + (global, { modal }): StateProps => { + const shouldForceHeight = modal?.currency === TON_CURRENCY_CODE + ? Boolean(global.ton?.history?.all?.transactions.length) + : Boolean(global.stars?.history?.all?.transactions.length); return { shouldForceHeight, starsBalanceState: global.stars, + tonBalanceState: global.ton, canBuyPremium: !selectIsPremiumPurchaseBlocked(global), + tonUsdRate: global.appConfig?.tonUsdRate || TON_USD_RATE_DEFAULT, + tonTopupUrl: global.appConfig?.tonTopupUrl || TON_TOPUP_URL_DEFAULT, }; }, )(StarsBalanceModal)); diff --git a/src/components/modals/stars/helpers/transaction.ts b/src/components/modals/stars/helpers/transaction.ts index c3feca92a..8918af192 100644 --- a/src/components/modals/stars/helpers/transaction.ts +++ b/src/components/modals/stars/helpers/transaction.ts @@ -1,6 +1,7 @@ -import type { ApiStarsAmount, ApiStarsTransaction } from '../../../../api/types'; +import type { ApiStarsAmount, ApiStarsTransaction, ApiTypeCurrencyAmount } from '../../../../api/types'; import type { OldLangFn } from '../../../../hooks/useOldLang'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments'; import { type LangFn, @@ -20,7 +21,7 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio } if (transaction.isGiftResale) { - return isNegativeStarsAmount(transaction.stars) + return isNegativeAmount(transaction.amount) ? lang('StarGiftSaleTransaction') : lang('StarGiftPurchaseTransaction'); } @@ -34,9 +35,14 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio if (transaction.isReaction) return oldLang('StarsReactionsSent'); if (transaction.giveawayPostId) return oldLang('StarsGiveawayPrizeReceived'); if (transaction.isMyGift) return oldLang('StarsGiftSent'); - if (transaction.isGift) return oldLang('StarsGiftReceived'); + if (transaction.isGift) { + if (transaction.amount.currency === TON_CURRENCY_CODE) { + return lang('TonGiftReceived'); + } + return oldLang('StarsGiftReceived'); + } if (transaction.starGift) { - return isNegativeStarsAmount(transaction.stars) ? oldLang('Gift2TransactionSent') : oldLang('Gift2ConvertedTitle'); + return isNegativeAmount(transaction.amount) ? oldLang('Gift2TransactionSent') : oldLang('Gift2ConvertedTitle'); } const customPeer = (transaction.peer && transaction.peer.type !== 'peer' @@ -51,3 +57,10 @@ export function isNegativeStarsAmount(starsAmount: ApiStarsAmount) { if (starsAmount.amount) return starsAmount.amount < 0; return starsAmount.nanos < 0; } + +export function isNegativeAmount(currencyAmount: ApiTypeCurrencyAmount) { + if (currencyAmount.currency === STARS_CURRENCY_CODE) { + return isNegativeStarsAmount(currencyAmount); + } + return currencyAmount.amount < 0; +} diff --git a/src/components/modals/stars/transaction/StarsTransactionItem.tsx b/src/components/modals/stars/transaction/StarsTransactionItem.tsx index 0f8b5a196..e031c7300 100644 --- a/src/components/modals/stars/transaction/StarsTransactionItem.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionItem.tsx @@ -8,6 +8,7 @@ import type { import type { GlobalState } from '../../../../global/types'; import type { CustomPeer } from '../../../../types'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments'; import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; @@ -16,7 +17,7 @@ import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; import { CUSTOM_PEER_PREMIUM } from '../../../../util/objects/customPeer'; import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; import renderText from '../../../common/helpers/renderText'; -import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction'; +import { getTransactionTitle, isNegativeAmount } from '../helpers/transaction'; import useSelector from '../../../../hooks/data/useSelector'; import useLang from '../../../../hooks/useLang'; @@ -48,7 +49,7 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => { const { openStarsTransactionModal } = getActions(); const { date, - stars, + amount, photo, peer: transactionPeer, extendedMedia, @@ -73,7 +74,10 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => { description = peer && getPeerTitle(oldLang, peer); avatarPeer = peer || CUSTOM_PEER_PREMIUM; } else { - const customPeer = buildStarsTransactionCustomPeer(transaction.peer); + const customPeer = buildStarsTransactionCustomPeer( + transaction.peer, + transaction.amount.currency === TON_CURRENCY_CODE, + ); title = customPeer.title || oldLang(customPeer.titleKey!); description = oldLang(customPeer.subtitleKey!); avatarPeer = customPeer; @@ -178,11 +182,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
- {formatStarsTransactionAmount(lang, stars)} + {formatStarsTransactionAmount(lang, amount)} - + {amount.currency === STARS_CURRENCY_CODE && }
); diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.tsx index f4b4d2c0b..4b4bc623c 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.tsx @@ -9,6 +9,7 @@ import type { import type { TabState } from '../../../../global/types'; import { MediaViewerOrigin } from '../../../../types'; +import { STARS_CURRENCY_CODE } from '../../../../config'; import { getMessageLink } from '../../../../global/helpers'; import { buildStarsTransactionCustomPeer, @@ -17,6 +18,7 @@ import { import { selectCanPlayAnimatedEmojis, selectGiftStickerForStars, + selectGiftStickerForTon, selectPeer, } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; @@ -25,7 +27,7 @@ import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; import { formatStarsAsIcon } from '../../../../util/localization/format'; import { formatPercent } from '../../../../util/textFormat'; import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; -import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction'; +import { getTransactionTitle, isNegativeAmount } from '../helpers/transaction'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; @@ -85,7 +87,7 @@ const StarsTransactionModal: FC = ({ } const { - giveawayPostId, photo, stars, isGiftUpgrade, starGift, isGiftResale, + giveawayPostId, photo, amount, isGiftUpgrade, starGift, isGiftResale, starRefCommision, } = transaction; @@ -132,7 +134,7 @@ const StarsTransactionModal: FC = ({ modelAttribute={giftAttributes!.model!} title={gift.title} subtitle={lang('GiftInfoCollectible', { number: gift.number })} - resellPrice={transaction.stars} + resellPrice={transaction.amount} /> ); @@ -169,11 +171,11 @@ const StarsTransactionModal: FC = ({

{description}

- {formatStarsTransactionAmount(lang, stars)} + {formatStarsTransactionAmount(lang, amount)} - + {amount.currency === STARS_CURRENCY_CODE && } {transaction.isRefund && (

{lang('Refunded')}

)} @@ -212,7 +214,7 @@ const StarsTransactionModal: FC = ({ if (isGiftResale) { tableData.push([ oldLang('StarGiftReason'), - isNegativeStarsAmount(transaction.stars) + isNegativeAmount(transaction.amount) ? lang('StarGiftSaleTransaction') : lang('StarGiftPurchaseTransaction'), ]); @@ -221,7 +223,7 @@ const StarsTransactionModal: FC = ({ let peerLabel; if (isGiftUpgrade) { peerLabel = oldLang('Stars.Transaction.GiftFrom'); - } else if (isNegativeStarsAmount(stars) || transaction.isMyGift) { + } else if (isNegativeAmount(amount) || transaction.isMyGift) { peerLabel = oldLang('Stars.Transaction.To'); } else if (transaction.starRefCommision && !transaction.paidMessages && !isGiftResale) { peerLabel = oldLang('StarsTransaction.StarRefReason.Miniapp'); @@ -240,7 +242,7 @@ const StarsTransactionModal: FC = ({ tableData.push([ lang('PaidMessageTransactionTotal'), formatStarsAsIcon(lang, - transaction.stars.amount / ((100 - transaction.starRefCommision) / 100), + transaction.amount.amount / ((100 - transaction.starRefCommision) / 100), { asFont: false, className: styles.starIcon, containerClassName: styles.totalStars }), ]); } @@ -249,9 +251,9 @@ const StarsTransactionModal: FC = ({ tableData.push([oldLang('Stars.Transaction.Reaction.Post'), ]); } - if (giveawayMessageLink) { + if (giveawayMessageLink && transaction.amount.currency === STARS_CURRENCY_CODE) { tableData.push([oldLang('BoostReason'), ]); - tableData.push([oldLang('Gift'), oldLang('Stars', transaction.stars, 'i')]); + tableData.push([oldLang('Gift'), oldLang('Stars', transaction.amount, 'i')]); } if (transaction.id) { @@ -322,8 +324,10 @@ export default memo(withGlobal( const peer = peerId ? selectPeer(global, peerId) : undefined; const paidMessageCommission = global.appConfig?.starsPaidMessageCommissionPermille; - const starCount = modal?.transaction.stars; - const starsGiftSticker = modal?.transaction.isGift && selectGiftStickerForStars(global, starCount?.amount); + const currencyAmount = modal?.transaction.amount; + const starsGiftSticker = modal?.transaction.isGift + && currencyAmount?.currency === STARS_CURRENCY_CODE ? selectGiftStickerForStars(global, currencyAmount?.amount) + : selectGiftStickerForTon(global, currencyAmount?.amount); return { peer, diff --git a/src/components/modals/suggestMessage/SuggestMessageModal.module.scss b/src/components/modals/suggestMessage/SuggestMessageModal.module.scss index 1afc09de9..557be77fc 100644 --- a/src/components/modals/suggestMessage/SuggestMessageModal.module.scss +++ b/src/components/modals/suggestMessage/SuggestMessageModal.module.scss @@ -53,3 +53,14 @@ .offerButton { font-weight: var(--font-weight-medium); } + +.currencySelector { + display: flex; + gap: 0.5rem; + justify-content: center; + margin-bottom: 1.25rem; +} + +.currencyIcon { + margin-inline-end: 0.25rem; +} diff --git a/src/components/modals/suggestMessage/SuggestMessageModal.tsx b/src/components/modals/suggestMessage/SuggestMessageModal.tsx index 779635ebd..d602ab689 100644 --- a/src/components/modals/suggestMessage/SuggestMessageModal.tsx +++ b/src/components/modals/suggestMessage/SuggestMessageModal.tsx @@ -4,22 +4,31 @@ import { useState } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiDraft, ApiStarsAmount } from '../../../api/types'; +import type { ApiDraft, ApiStarsAmount, ApiTypeCurrencyAmount } from '../../../api/types'; import type { ApiPeer } from '../../../api/types'; import type { TabState } from '../../../global/types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { + STARS_CURRENCY_CODE, STARS_SUGGESTED_POST_AGE_MIN, STARS_SUGGESTED_POST_AMOUNT_MAX, STARS_SUGGESTED_POST_AMOUNT_MIN, STARS_SUGGESTED_POST_FUTURE_MAX, - STARS_SUGGESTED_POST_FUTURE_MIN } from '../../../config'; -import { selectPeer } from '../../../global/selectors'; + STARS_SUGGESTED_POST_FUTURE_MIN, + TON_CURRENCY_CODE, + TON_SUGGESTED_POST_AMOUNT_MAX, + TON_SUGGESTED_POST_AMOUNT_MIN } from '../../../config'; +import { selectIsMonoforumAdmin, selectPeer } from '../../../global/selectors'; import { selectDraft } from '../../../global/selectors/messages'; import buildClassName from '../../../util/buildClassName'; import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat'; -import { formatStarsAsIcon, formatStarsAsText } from '../../../util/localization/format'; +import { convertTonFromNanos, convertTonToNanos } from '../../../util/formatCurrency'; +import { + formatStarsAsIcon, + formatStarsAsText, + formatTonAsIcon, + formatTonAsText } from '../../../util/localization/format'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -41,25 +50,33 @@ import useFlag from '../../../hooks/useFlag'; type StateProps = { starBalance?: ApiStarsAmount; + tonBalance?: number; peer?: ApiPeer; currentDraft?: ApiDraft; - maxAmount: number; - minAmount: number; + maxStarsAmount: number; + minStarsAmount: number; + tonMaxAmount: number; + tonMinAmount: number; ageMinSeconds: number; futureMin: number; futureMax: number; + isMonoforumAdmin?: boolean; }; const SuggestMessageModal = ({ modal, starBalance, + tonBalance, peer, currentDraft, - maxAmount, - minAmount, + maxStarsAmount, + minStarsAmount, + tonMaxAmount, + tonMinAmount, ageMinSeconds, futureMin, futureMax, + isMonoforumAdmin, }: OwnProps & StateProps) => { const { closeSuggestMessageModal, updateDraftSuggestedPostInfo, openStarsBalanceModal } = getActions(); const [isCalendarOpened, openCalendar, closeCalendar] = useFlag(); @@ -68,9 +85,12 @@ const SuggestMessageModal = ({ const currentReplyInfo = currentDraft?.replyInfo; const isInSuggestChangesMode = Boolean(currentReplyInfo); - const [starsAmount, setStarsAmount] = useState( + const [currencyAmount, setCurrencyAmount] = useState( currentSuggestedPostInfo?.price?.amount || undefined, ); + const [selectedCurrency, setSelectedCurrency] = useState( + currentSuggestedPostInfo?.price?.currency || STARS_CURRENCY_CODE, + ); const [scheduleDate, setScheduleDate] = useState( currentSuggestedPostInfo?.scheduleDate ? currentSuggestedPostInfo.scheduleDate * 1000 @@ -78,7 +98,10 @@ const SuggestMessageModal = ({ ); useEffect(() => { - setStarsAmount(currentSuggestedPostInfo?.price?.amount || undefined); + const price = currentSuggestedPostInfo?.price; + const amount = price?.currency === TON_CURRENCY_CODE ? convertTonFromNanos(price.amount) : price?.amount; + setCurrencyAmount(amount); + setSelectedCurrency(currentSuggestedPostInfo?.price?.currency || STARS_CURRENCY_CODE); setScheduleDate(currentSuggestedPostInfo?.scheduleDate ? currentSuggestedPostInfo.scheduleDate * 1000 : undefined); @@ -87,6 +110,7 @@ const SuggestMessageModal = ({ const lang = useLang(); const oldLang = useOldLang(); + const isCurrencyStars = selectedCurrency === STARS_CURRENCY_CODE; const now = Math.floor(Date.now() / 1000); const minAt = (now + futureMin) * 1000; const maxAt = (now + futureMax) * 1000; @@ -97,9 +121,9 @@ const SuggestMessageModal = ({ const number = parseFloat(value); const result = value === '' || Number.isNaN(number) ? undefined - : Math.min(Math.max(number, 0), maxAmount); + : Math.min(Math.max(number, 0), currentMaxAmount); - setStarsAmount(result); + setCurrencyAmount(result); }); const handleExpireDateChange = useLastCallback((date: Date) => { @@ -112,28 +136,44 @@ const SuggestMessageModal = ({ closeCalendar(); }); - const isDisabled = Boolean(starsAmount) && starsAmount < minAmount; + const currentMinAmount = isCurrencyStars ? minStarsAmount : convertTonFromNanos(tonMinAmount); + const currentMaxAmount = isCurrencyStars ? maxStarsAmount : convertTonFromNanos(tonMaxAmount); + const isDisabled = Boolean(currencyAmount) && currencyAmount < currentMinAmount; const handleOffer = useLastCallback(() => { - const neededAmount = starsAmount || 0; + const neededAmount = currencyAmount + ? (isCurrencyStars ? currencyAmount : convertTonToNanos(currencyAmount)) + : 0; if (isDisabled) { return; } - const currentBalance = starBalance?.amount || 0; + if (!isMonoforumAdmin) { + if (isCurrencyStars) { + const currentBalance = starBalance?.amount || 0; - if (neededAmount > currentBalance) { - openStarsBalanceModal({ - topup: { - balanceNeeded: neededAmount, - }, - }); - return; + if (neededAmount > currentBalance) { + openStarsBalanceModal({ + topup: { + balanceNeeded: neededAmount, + }, + }); + return; + } + } else { + const currentTonBalance = tonBalance || 0; + if (neededAmount > currentTonBalance) { + openStarsBalanceModal({ + currency: TON_CURRENCY_CODE, + }); + return; + } + } } updateDraftSuggestedPostInfo({ - price: { amount: neededAmount, nanos: 0 }, + price: { currency: selectedCurrency, amount: neededAmount, nanos: 0 }, scheduleDate: scheduleDate ? scheduleDate / 1000 : undefined, }); @@ -153,23 +193,51 @@ const SuggestMessageModal = ({ >
+
+ + +
- {starsAmount !== undefined && starsAmount > 0 && starsAmount < minAmount + {currencyAmount !== undefined && currencyAmount > 0 && currencyAmount < currentMinAmount ? lang('DescriptionSuggestedPostMinimumOffer', { - amount: formatStarsAsText(lang, minAmount) }, + amount: isCurrencyStars + ? formatStarsAsText(lang, currentMinAmount) + : formatTonAsText(lang, currentMinAmount) }, { withNodes: true, withMarkdown: true }) - : lang('SuggestMessagePriceDescription', { - currency: lang('CurrencyStars'), - })} + : isCurrencyStars + ? lang('SuggestMessagePriceDescriptionStars') + : lang('SuggestMessagePriceDescriptionTon')}
@@ -178,7 +246,9 @@ const SuggestMessageModal = ({ @@ -217,8 +287,10 @@ const SuggestMessageModal = ({ disabled={isDisabled} > {isInSuggestChangesMode ? lang('ButtonUpdateTerms') - : starsAmount ? lang('ButtonOfferAmount', { - amount: formatStarsAsIcon(lang, starsAmount, { asFont: true }), + : currencyAmount ? lang('ButtonOfferAmount', { + amount: isCurrencyStars + ? formatStarsAsIcon(lang, currencyAmount, { asFont: true }) + : formatTonAsIcon(lang, currencyAmount, { asFont: true }), }, { withNodes: true, withMarkdown: true, @@ -236,21 +308,30 @@ export default memo(withGlobal( const currentDraft = modal ? selectDraft(global, modal.chatId, MAIN_THREAD_ID) : undefined; const { appConfig } = global; - const maxAmount = appConfig?.starsSuggestedPostAmountMax || STARS_SUGGESTED_POST_AMOUNT_MAX; - const minAmount = appConfig?.starsSuggestedPostAmountMin || STARS_SUGGESTED_POST_AMOUNT_MIN; + const maxStarsAmount = appConfig?.starsSuggestedPostAmountMax || STARS_SUGGESTED_POST_AMOUNT_MAX; + const minStarsAmount = appConfig?.starsSuggestedPostAmountMin || STARS_SUGGESTED_POST_AMOUNT_MIN; const ageMinSeconds = appConfig?.starsSuggestedPostAgeMin || STARS_SUGGESTED_POST_AGE_MIN; const futureMin = appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN; const futureMax = appConfig?.starsSuggestedPostFutureMax || STARS_SUGGESTED_POST_FUTURE_MAX; + const tonMaxAmount = appConfig?.tonSuggestedPostAmountMax || TON_SUGGESTED_POST_AMOUNT_MAX; + const tonMinAmount = appConfig?.tonSuggestedPostAmountMin || TON_SUGGESTED_POST_AMOUNT_MIN; + + const isMonoforumAdmin = modal ? selectIsMonoforumAdmin(global, modal.chatId) : false; + return { peer, starBalance, + tonBalance: global.ton?.balance?.amount, currentDraft, - maxAmount, - minAmount, + maxStarsAmount, + minStarsAmount, + tonMaxAmount, + tonMinAmount, ageMinSeconds, futureMin, futureMax, + isMonoforumAdmin, }; }, )(SuggestMessageModal)); diff --git a/src/components/modals/suggestedPostApproval/SuggestedPostApprovalModal.tsx b/src/components/modals/suggestedPostApproval/SuggestedPostApprovalModal.tsx index a54d011f6..88369aea3 100644 --- a/src/components/modals/suggestedPostApproval/SuggestedPostApprovalModal.tsx +++ b/src/components/modals/suggestedPostApproval/SuggestedPostApprovalModal.tsx @@ -4,15 +4,18 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiMessage, ApiPeer } from '../../../api/types'; import type { TabState } from '../../../global/types'; -import { STARS_SUGGESTED_POST_AGE_MIN, +import { STARS_CURRENCY_CODE, STARS_SUGGESTED_POST_AGE_MIN, STARS_SUGGESTED_POST_COMMISSION_PERMILLE, STARS_SUGGESTED_POST_FUTURE_MAX, STARS_SUGGESTED_POST_FUTURE_MIN, + TON_CURRENCY_CODE, + TON_SUGGESTED_POST_COMMISSION_PERMILLE, } from '../../../config'; import { getPeerFullTitle } from '../../../global/helpers/peers'; import { selectChatMessage, selectIsMonoforumAdmin, selectSender } from '../../../global/selectors'; import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat'; -import { formatStarsAsText } from '../../../util/localization/format'; +import { convertTonFromNanos } from '../../../util/formatCurrency'; +import { formatStarsAsText, formatTonAsText } from '../../../util/localization/format'; import renderText from '../../common/helpers/renderText'; import useFlag from '../../../hooks/useFlag'; @@ -31,6 +34,7 @@ export type OwnProps = { type StateProps = { commissionPermille: number; + tonCommissionPermille: number; minAge: number; futureMin: number; futureMax: number; @@ -46,6 +50,7 @@ const SuggestedPostApprovalModal = ({ sender, isAdmin, commissionPermille, + tonCommissionPermille, minAge, futureMin, futureMax, @@ -114,7 +119,10 @@ const SuggestedPostApprovalModal = ({ const senderName = sender ? getPeerFullTitle(oldLang, sender) : ''; const renderContent = () => { - const amount = message?.suggestedPostInfo?.price?.amount; + const price = message?.suggestedPostInfo?.price; + const amount = price?.amount; + const currency = price?.currency || STARS_CURRENCY_CODE; + const question = lang( 'SuggestedPostConfirmMessage', { peer: senderName }, @@ -125,10 +133,13 @@ const SuggestedPostApprovalModal = ({ return questionText; } - const commission = (commissionPermille / 10); + const currentCommissionPermille = currency === TON_CURRENCY_CODE ? tonCommissionPermille : commissionPermille; + const commission = (currentCommissionPermille / 10); const amountWithCommission = amount / 100 * commission; - const starsText = formatStarsAsText(lang, amountWithCommission); + const formattedAmount = currency === TON_CURRENCY_CODE + ? formatTonAsText(lang, convertTonFromNanos(amountWithCommission)) + : formatStarsAsText(lang, amountWithCommission); const ageMinSeconds = minAge; const duration = formatShortDuration(lang, ageMinSeconds, true); @@ -146,7 +157,7 @@ const SuggestedPostApprovalModal = ({
{renderText(lang(key, { - amount: starsText, + amount: formattedAmount, commission, duration, time, @@ -165,7 +176,7 @@ const SuggestedPostApprovalModal = ({
{renderText(lang(key, { - amount: starsText, + amount: formattedAmount, commission, duration, }, { withNodes: true, withMarkdown: true }))} @@ -215,6 +226,8 @@ export default memo(withGlobal( const { appConfig } = global; const commissionPermille = appConfig?.starsSuggestedPostCommissionPermille || STARS_SUGGESTED_POST_COMMISSION_PERMILLE; + const tonCommissionPermille = appConfig?.tonSuggestedPostCommissionPermille + || TON_SUGGESTED_POST_COMMISSION_PERMILLE; const minAge = appConfig?.starsSuggestedPostAgeMin || STARS_SUGGESTED_POST_AGE_MIN; const futureMin = (appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN) * 2; const futureMax = appConfig?.starsSuggestedPostFutureMax || STARS_SUGGESTED_POST_FUTURE_MAX; @@ -228,6 +241,7 @@ export default memo(withGlobal( sender, isAdmin, commissionPermille, + tonCommissionPermille, scheduleDate, }; }, diff --git a/src/config.ts b/src/config.ts index eb5d1c36e..66e30256a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -117,7 +117,12 @@ export const STARS_SUGGESTED_POST_COMMISSION_PERMILLE = 850; export const STARS_SUGGESTED_POST_AGE_MIN = 86400; // 24 hours in seconds export const STARS_SUGGESTED_POST_FUTURE_MAX = 2678400; // 31 days in seconds export const STARS_SUGGESTED_POST_FUTURE_MIN = 300; // 5 minutes in seconds +export const TON_CURRENCY_CODE = 'TON'; export const TON_SUGGESTED_POST_COMMISSION_PERMILLE = 850; +export const TON_USD_RATE_DEFAULT = 3; +export const TON_TOPUP_URL_DEFAULT = 'https://fragment.com/ads/topup'; +export const TON_SUGGESTED_POST_AMOUNT_MIN = 10000000; // 0.01 TON in nanos +export const TON_SUGGESTED_POST_AMOUNT_MAX = 10000000000000; // 10 000 TON in nanos export const STORY_VIEWS_MIN_SEARCH = 15; export const STORY_MIN_REACTIONS_SORT = 10; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 74614c812..e7691af90 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -33,10 +33,12 @@ import { MESSAGE_LIST_SLICE, RE_TELEGRAM_LINK, SERVICE_NOTIFICATIONS_USER_ID, + STARS_CURRENCY_CODE, STARS_SUGGESTED_POST_FUTURE_MIN, SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_PHOTO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, + TON_CURRENCY_CODE, } from '../../../config'; import { ensureProtocol, isMixedScriptUrl } from '../../../util/browser/url'; import { IS_IOS } from '../../../util/browser/windowEnvironment'; @@ -361,18 +363,31 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise const messagePriceInStars = await getPeerStarsForMessage(global, chatId!); - const suggestedPostPrice = draftSuggestedPostInfo?.price?.amount || 0; - if (suggestedPostPrice && !draftReplyInfo) { - const currentBalance = global.stars?.balance?.amount || 0; + const suggestedPostPrice = draftSuggestedPostInfo?.price; + const suggestedPostCurrency = suggestedPostPrice?.currency || STARS_CURRENCY_CODE; + const suggestedPostAmount = suggestedPostPrice?.amount || 0; + if (suggestedPostAmount && !draftReplyInfo) { + if (suggestedPostCurrency === STARS_CURRENCY_CODE) { + const currentBalance = global.stars?.balance?.amount || 0; - if (suggestedPostPrice > currentBalance) { - actions.openStarsBalanceModal({ - topup: { - balanceNeeded: suggestedPostPrice, - }, - tabId, - }); - return; + if (suggestedPostAmount > currentBalance) { + actions.openStarsBalanceModal({ + topup: { + balanceNeeded: suggestedPostAmount, + }, + tabId, + }); + return; + } + } else if (suggestedPostCurrency === TON_CURRENCY_CODE) { + const currentTonBalance = global.ton?.balance?.amount || 0; + if (suggestedPostAmount > currentTonBalance) { + actions.openStarsBalanceModal({ + currency: TON_CURRENCY_CODE, + tabId, + }); + return; + } } } @@ -2203,16 +2218,28 @@ addActionHandler('approveSuggestedPost', async (global, actions, payload): Promi if (!isAdmin && message?.suggestedPostInfo?.price?.amount) { const neededAmount = message.suggestedPostInfo.price.amount; - const currentBalance = global.stars?.balance?.amount || 0; + const isCurrencyStars = message.suggestedPostInfo.price.currency === STARS_CURRENCY_CODE; - if (neededAmount > currentBalance) { - actions.openStarsBalanceModal({ - topup: { - balanceNeeded: neededAmount, - }, - tabId, - }); - return; + if (isCurrencyStars) { + const currentBalance = global.stars?.balance?.amount || 0; + if (neededAmount > currentBalance) { + actions.openStarsBalanceModal({ + topup: { + balanceNeeded: neededAmount, + }, + tabId, + }); + return; + } + } else { + const currentTonBalance = global.ton?.balance?.amount || 0; + if (neededAmount > currentTonBalance) { + actions.openStarsBalanceModal({ + currency: TON_CURRENCY_CODE, + tabId, + }); + return; + } } } diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index 127f0a797..76d41e2ab 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -2,7 +2,12 @@ import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types'; import type { StarGiftCategory } from '../../../types'; import type { ActionReturnType } from '../../types'; -import { DEFAULT_RESALE_GIFTS_FILTER_OPTIONS, RESALE_GIFTS_LIMIT } from '../../../config'; +import { + DEFAULT_RESALE_GIFTS_FILTER_OPTIONS, + RESALE_GIFTS_LIMIT, + STARS_CURRENCY_CODE, + TON_CURRENCY_CODE, +} from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey } from '../../../util/iteratees'; import { callApi } from '../../../api/gramjs'; @@ -26,57 +31,82 @@ import { } from '../../selectors'; addActionHandler('loadStarStatus', async (global): Promise => { - const currentStatus = global.stars; - const needsTopupOptions = !currentStatus?.topupOptions; + const currentStarsStatus = global.stars; + const needsTopupOptions = !currentStarsStatus?.topupOptions; - const [status, topupOptions] = await Promise.all([ + const [starsStatus, tonStatus, topupOptions] = await Promise.all([ callApi('fetchStarsStatus'), + callApi('fetchStarsStatus', { isTon: true }), needsTopupOptions ? callApi('fetchStarsTopupOptions') : undefined, ]); - if (!status || (needsTopupOptions && !topupOptions)) { + if (!(starsStatus || tonStatus) || (needsTopupOptions && !topupOptions)) { return; } global = getGlobal(); - global = { - ...global, - stars: { - ...currentStatus, - balance: status.balance, - topupOptions: topupOptions || currentStatus!.topupOptions, - history: { - all: undefined, - inbound: undefined, - outbound: undefined, + if (starsStatus && starsStatus.balance.currency === STARS_CURRENCY_CODE) { + global = { + ...global, + stars: { + ...currentStarsStatus, + balance: starsStatus.balance, + topupOptions: topupOptions || currentStarsStatus!.topupOptions, + history: { + all: undefined, + inbound: undefined, + outbound: undefined, + }, + subscriptions: undefined, }, - subscriptions: undefined, - }, - }; + }; - if (status.history) { - global = appendStarsTransactions(global, 'all', status.history, status.nextHistoryOffset); + if (starsStatus.history) { + global = appendStarsTransactions(global, 'all', starsStatus.history, starsStatus.nextHistoryOffset); + } + + if (starsStatus.subscriptions) { + global = appendStarsSubscriptions(global, starsStatus.subscriptions, starsStatus.nextSubscriptionOffset); + } } - if (status.subscriptions) { - global = appendStarsSubscriptions(global, status.subscriptions, status.nextSubscriptionOffset); + if (tonStatus?.balance.currency === TON_CURRENCY_CODE) { + global = { + ...global, + ton: { + ...tonStatus, + balance: tonStatus.balance, + history: { + all: undefined, + inbound: undefined, + outbound: undefined, + }, + }, + }; + + global = updateStarsBalance(global, tonStatus.balance); + + if (tonStatus.history) { + global = appendStarsTransactions(global, 'all', tonStatus.history, tonStatus.nextHistoryOffset, true); + } } setGlobal(global); }); addActionHandler('loadStarsTransactions', async (global, actions, payload): Promise => { - const { type } = payload; + const { type, isTon } = payload; - const history = global.stars?.history[type]; + const history = isTon ? global.ton?.history[type] : global.stars?.history[type]; const offset = history?.nextOffset; if (history && !offset) return; // Already loaded all const result = await callApi('fetchStarsTransactions', { - isInbound: type === 'inbound' || undefined, - isOutbound: type === 'outbound' || undefined, + isInbound: type === 'inbound', + isOutbound: type === 'outbound', offset: offset || '', + isTon, }); if (!result) { @@ -87,7 +117,7 @@ addActionHandler('loadStarsTransactions', async (global, actions, payload): Prom global = updateStarsBalance(global, result.balance); if (result.history) { - global = appendStarsTransactions(global, type, result.history, result.nextOffset); + global = appendStarsTransactions(global, type, result.history, result.nextOffset, isTon); } setGlobal(global); }); @@ -315,7 +345,7 @@ addActionHandler('loadStarsSubscriptions', async (global): Promise => { offset: offset || '', }); - if (!result) { + if (!result || result.balance.currency !== STARS_CURRENCY_CODE) { return; } diff --git a/src/global/actions/api/symbols.ts b/src/global/actions/api/symbols.ts index 5c89c2ba5..d29272e2f 100644 --- a/src/global/actions/api/symbols.ts +++ b/src/global/actions/api/symbols.ts @@ -215,6 +215,22 @@ addActionHandler('loadPremiumGifts', async (global): Promise => { setGlobal(global); }); +addActionHandler('loadTonGifts', async (global): Promise => { + const stickerSet = await callApi('fetchTonGifts'); + if (!stickerSet) { + return; + } + + const { set, stickers } = stickerSet; + + global = getGlobal(); + global = { + ...global, + tonGifts: { ...set, stickers }, + }; + setGlobal(global); +}); + addActionHandler('loadDefaultTopicIcons', async (global): Promise => { const stickerSet = await callApi('fetchDefaultTopicIcons'); if (!stickerSet) { diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 40477c06c..7cbed76e3 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -1,6 +1,7 @@ import type { ApiInputSavedStarGift, ApiSavedStarGift } from '../../../api/types'; import type { ActionReturnType } from '../../types'; +import { STARS_CURRENCY_CODE } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import * as langProvider from '../../../util/oldLangProvider'; import { addTabStateResetterAction } from '../../helpers/meta'; @@ -110,6 +111,7 @@ addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionRetu originGift, topup, shouldIgnoreBalance, + currency = STARS_CURRENCY_CODE, tabId = getCurrentTabId(), } = payload || {}; @@ -140,6 +142,7 @@ addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionRetu originReaction, originGift, topup, + currency, }, }, tabId); }); diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index fdcdc6990..827ddf7c4 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -8,12 +8,15 @@ import type { ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarsTransactionPeerPeer, + ApiTypeCurrencyAmount, } from '../../api/types'; import type { CustomPeer } from '../../types'; import type { LangFn } from '../../util/localization'; import type { GlobalState } from '../types'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../config'; import arePropsShallowEqual from '../../util/arePropsShallowEqual'; +import { convertCurrencyFromBaseUnit } from '../../util/formatCurrency'; import { selectChat, selectPeer, selectUser } from '../selectors'; export function getRequestInputInvoice( @@ -257,6 +260,7 @@ export function getRequestInputSavedStarGift( export function buildStarsTransactionCustomPeer( peer: Exclude, + isForTon?: boolean, ): CustomPeer { if (peer.type === 'appStore') { return { @@ -279,8 +283,17 @@ export function buildStarsTransactionCustomPeer( } if (peer.type === 'fragment') { + if (isForTon) { + return { + avatarIcon: 'fragment', + isCustomPeer: true, + titleKey: 'Stars.Gift.Received.Title', + subtitleKey: 'Stars.Intro.Transaction.Gift.UnknownUser', + customPeerAvatarColor: '#000000', + }; + } return { - avatarIcon: 'star', + avatarIcon: 'fragment', isCustomPeer: true, titleKey: 'Stars.Intro.Transaction.FragmentTopUp.Title', subtitleKey: 'Stars.Intro.Transaction.FragmentTopUp.Subtitle', @@ -328,13 +341,29 @@ export function buildStarsTransactionCustomPeer( }; } -export function formatStarsTransactionAmount(lang: LangFn, starsAmount: ApiStarsAmount) { - const amount = starsAmount.amount + starsAmount.nanos / 1e9; - if (amount < 0) { - return `- ${lang.number(Math.abs(amount))}`; +export function formatStarsTransactionAmount(lang: LangFn, currencyAmount: ApiTypeCurrencyAmount) { + if (currencyAmount.currency === STARS_CURRENCY_CODE) { + const amount = currencyAmount.amount + currencyAmount.nanos / 1e9; + if (amount < 0) { + return `- ${lang.number(Math.abs(amount))}`; + } + + return `+ ${lang.number(amount)}`; } - return `+ ${lang.number(amount)}`; + if (currencyAmount.currency === TON_CURRENCY_CODE) { + const amount = convertCurrencyFromBaseUnit(currencyAmount.amount, currencyAmount.currency); + const absAmount = Math.abs(amount); + const tonText = lang('TonAmountText', { amount: absAmount }, { pluralValue: absAmount }); + + if (amount < 0) { + return `- ${tonText}`; + } + + return `+ ${tonText}`; + } + + return undefined; } export function formatStarsAmount(lang: LangFn, starsAmount: ApiStarsAmount) { @@ -344,24 +373,45 @@ export function formatStarsAmount(lang: LangFn, starsAmount: ApiStarsAmount) { export function getStarsTransactionFromGift(message: ApiMessage): ApiStarsTransaction | undefined { const { action } = message.content; - if (action?.type !== 'giftStars') return undefined; + if (action?.type === 'giftStars') { + const { transactionId, stars } = action; - const { transactionId, stars } = action; + return { + id: transactionId, + amount: { + currency: STARS_CURRENCY_CODE, + amount: stars, + nanos: 0, + }, + peer: { + type: 'peer', + id: message.isOutgoing ? message.chatId : (message.senderId || message.chatId), + }, + date: message.date, + isGift: true, + isMyGift: message.isOutgoing || undefined, + }; + } - return { - id: transactionId, - stars: { - amount: stars, - nanos: 0, - }, - peer: { - type: 'peer', - id: message.isOutgoing ? message.chatId : (message.senderId || message.chatId), - }, - date: message.date, - isGift: true, - isMyGift: message.isOutgoing || undefined, - }; + if (action?.type === 'giftTon') { + const { transactionId, cryptoAmount } = action; + + return { + id: transactionId, + amount: { + currency: TON_CURRENCY_CODE, + amount: cryptoAmount, + }, + peer: { + type: 'fragment', + }, + date: message.date, + isGift: true, + isMyGift: message.isOutgoing || undefined, + }; + } + + return undefined; } export function getPrizeStarsTransactionFromGiveaway(message: ApiMessage): ApiStarsTransaction | undefined { @@ -373,7 +423,8 @@ export function getPrizeStarsTransactionFromGiveaway(message: ApiMessage): ApiSt return { id: transactionId, - stars: { + amount: { + currency: STARS_CURRENCY_CODE, amount: stars, nanos: 0, }, diff --git a/src/global/reducers/payments.ts b/src/global/reducers/payments.ts index 07889819a..19a6ad2ad 100644 --- a/src/global/reducers/payments.ts +++ b/src/global/reducers/payments.ts @@ -1,9 +1,9 @@ import type { ApiReceiptRegular, ApiReceiptStars, - ApiStarsAmount, ApiStarsSubscription, ApiStarsTransaction, + ApiTypeCurrencyAmount, } from '../../api/types'; import type { PaymentStep, @@ -15,6 +15,7 @@ import type { GlobalState, TabArgs, TabState, } from '../types'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../config'; import { getCurrentTabId } from '../../util/establishMultitabRole'; import { selectStarsPayment, selectTabState } from '../selectors'; import { updateTabState } from './tabs'; @@ -137,15 +138,29 @@ export function closeInvoice( } export function updateStarsBalance( - global: T, balance: ApiStarsAmount, + global: T, balance: ApiTypeCurrencyAmount, ): T { - return { - ...global, - stars: { - ...global.stars, - balance, - }, - }; + if (balance.currency === STARS_CURRENCY_CODE) { + return { + ...global, + stars: { + ...global.stars, + balance, + }, + }; + } + + if (balance.currency === TON_CURRENCY_CODE) { + return { + ...global, + ton: { + ...global.ton, + balance, + }, + }; + } + + return global; } export function appendStarsTransactions( @@ -153,7 +168,31 @@ export function appendStarsTransactions( type: StarsTransactionType, transactions: ApiStarsTransaction[], nextOffset?: string, + isTon?: boolean, ): T { + if (isTon) { + const history = global.ton?.history; + if (!history) { + return global; + } + + const newTypeObject = { + transactions: (history[type]?.transactions || []).concat(transactions), + nextOffset, + }; + + return { + ...global, + ton: { + ...global.ton, + history: { + ...history, + [type]: newTypeObject, + }, + }, + }; + } + const history = global.stars?.history; if (!history) { return global; @@ -238,7 +277,8 @@ export function openStarsTransactionFromReceipt( type: 'peer', id: receipt.botId, }, - stars: { + amount: { + currency: STARS_CURRENCY_CODE, amount: receipt.totalAmount, nanos: 0, }, diff --git a/src/global/selectors/symbols.ts b/src/global/selectors/symbols.ts index 86646ba1e..4ea6bcf4d 100644 --- a/src/global/selectors/symbols.ts +++ b/src/global/selectors/symbols.ts @@ -1,8 +1,9 @@ import type { ApiSticker, ApiStickerSet, ApiStickerSetInfo } from '../../api/types'; import type { GlobalState, TabArgs } from '../types'; -import { RESTRICTED_EMOJI_SET_ID } from '../../config'; +import { RESTRICTED_EMOJI_SET_ID, TON_CURRENCY_CODE } from '../../config'; import { getCurrentTabId } from '../../util/establishMultitabRole'; +import { convertCurrencyFromBaseUnit } from '../../util/formatCurrency'; import { selectTabState } from './tabs'; import { selectIsCurrentUserPremium } from './users'; @@ -21,6 +22,12 @@ const STAR_EMOTICON: Record = { 5000: `${4}\u{FE0F}\u20E3`, }; +const TON_EMOTICON: Record = { + 1: `${1}\u{FE0F}\u20E3`, + 10: `${2}\u{FE0F}\u20E3`, + 50: `${3}\u{FE0F}\u20E3`, +}; + export function selectIsStickerFavorite(global: T, sticker: ApiSticker) { const { stickers } = global.stickers.favorite; return stickers && stickers.some(({ id }) => id === sticker.id); @@ -194,3 +201,20 @@ export function selectGiftStickerForStars(global: T, star return stickers.find((sticker) => sticker.emoji === emoji) || stickers[0]; } + +export function selectGiftStickerForTon(global: T, amount?: number) { + const stickers = global.tonGifts?.stickers; + if (!stickers || !amount) return undefined; + const convertedAmount = convertCurrencyFromBaseUnit(amount, TON_CURRENCY_CODE); + + let emoji; + if (convertedAmount < 10) { + emoji = TON_EMOTICON[1]; + } else if (convertedAmount < 50) { + emoji = TON_EMOTICON[10]; + } else { + emoji = TON_EMOTICON[50]; + } + + return stickers.find((sticker) => sticker.emoji === emoji) || stickers[0]; +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 979a1bbf2..79a7b4e63 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -49,6 +49,7 @@ import type { ApiStickerSetInfo, ApiThemeParameters, ApiTodoItem, + ApiTypeCurrencyAmount, ApiTypePrepaidGiveaway, ApiUpdate, ApiUser, @@ -1278,6 +1279,7 @@ export interface ActionPayloads { loadStarStatus: undefined; loadStarsTransactions: { type: StarsTransactionType; + isTon?: boolean; }; loadStarsSubscriptions: undefined; changeStarsSubscription: { @@ -1302,6 +1304,7 @@ export interface ActionPayloads { purpose?: string; }; shouldIgnoreBalance?: boolean; + currency?: ApiTypeCurrencyAmount['currency']; } & WithTabId; closeStarsBalanceModal: WithTabId | undefined; @@ -2483,6 +2486,7 @@ export interface ActionPayloads { }; loadPremiumGifts: undefined; + loadTonGifts: undefined; loadStarGifts: undefined; updateResaleGiftsFilter: { filter: ResaleGiftsFilterOptions; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 122985d1d..4526e4d67 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -37,6 +37,7 @@ import type { ApiSticker, ApiStickerSet, ApiTimezone, + ApiTonAmount, ApiTranscription, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType, @@ -367,6 +368,7 @@ export type GlobalState = { defaultTopicIconsId?: string; defaultStatusIconsId?: string; premiumGifts?: ApiStickerSet; + tonGifts?: ApiStickerSet; emojiKeywords: Record; collectibleEmojiStatuses?: { @@ -452,6 +454,10 @@ export type GlobalState = { history: StarsTransactionHistory; subscriptions?: StarsSubscriptions; }; + ton?: { + balance: ApiTonAmount; + history: StarsTransactionHistory; + }; }; export type RequiredGlobalState = GlobalState & { _: never }; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 04c93b740..0f2c2c913 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -45,6 +45,7 @@ import type { ApiStarsTransaction, ApiStarTopupOption, ApiSticker, + ApiTypeCurrencyAmount, ApiTypePrepaidGiveaway, ApiTypeStoryView, ApiUser, @@ -795,6 +796,7 @@ export type TabState = { balanceNeeded: number; purpose?: string; }; + currency?: ApiTypeCurrencyAmount['currency']; }; giftInfoModal?: { diff --git a/src/styles/index.scss b/src/styles/index.scss index ba18a31e0..aa0d22c2f 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -318,10 +318,12 @@ body:not(.is-ios) { } // Increase specificity to override the default icon style +.ton-amount-icon.ton-amount-icon, .star-amount-icon.star-amount-icon { margin-inline-start: 0.375em; // Prevent sticking to the text without using `white-space: pre` margin-inline-end: 0.2em; // Prevent sticking to the text without using `white-space: pre` line-height: inherit; // Vertical centring + vertical-align: text-bottom; // As regular text } .shared-canvas-container { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 611df5183..047c21657 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -576,6 +576,7 @@ export interface LangPair { 'MenuStickers': undefined; 'MenuAnimations': undefined; 'MenuStars': undefined; + 'MenuTon': undefined; 'MenuSendGift': undefined; 'MenuTelegramFaq': undefined; 'MenuPrivacyPolicy': undefined; @@ -1549,9 +1550,13 @@ export interface LangPair { 'TitleTime': undefined; 'TitleSuggestMessage': undefined; 'TitleSuggestedChanges': undefined; + 'SuggestMessageNoPrice': undefined; 'EnterPriceInStars': undefined; + 'EnterPriceInTon': undefined; + 'SuggestMessagePriceDescriptionStars': undefined; + 'SuggestMessagePriceDescriptionTon': undefined; 'SuggestMessageDateTimeHint': undefined; - 'TitleAnytime': undefined; + 'SuggestMessageAnytime': undefined; 'ButtonOfferFree': undefined; 'ButtonUpdateTerms': undefined; 'InputPlaceholderPrice': undefined; @@ -1563,6 +1568,7 @@ export interface LangPair { 'SuggestedPostRejectedNotification': undefined; 'SuggestedPostAgreementReached': undefined; 'CurrencyStars': undefined; + 'CurrencyTon': undefined; 'DeclineReasonPlaceholder': undefined; 'SuggestedPostRejectedYou': undefined; 'SuggestedPostRejectedWithReasonYou': undefined; @@ -1590,6 +1596,10 @@ export interface LangPair { 'ToDoListErrorChooseTitle': undefined; 'ToDoListErrorChooseTasks': undefined; 'PremiumPreviewTodo': undefined; + 'DescriptionAboutTon': undefined; + 'ButtonTopUpViaFragment': undefined; + 'TonModalHint': undefined; + 'TonGiftReceived': undefined; } export interface LangPairWithVariables { @@ -2425,6 +2435,13 @@ export interface LangPairWithVariables { 'from': V; 'amount': V; }; + 'TonAmount': { + 'amount': V; + }; + 'ActionGiftCostCrypto': { + 'cryptoPrice': V; + 'price': V; + }; 'ActionPaymentRefunded': { 'peer': V; 'amount': V; @@ -2584,9 +2601,6 @@ export interface LangPairWithVariables { 'user': V; 'changes': V; }; - 'SuggestMessagePriceDescription': { - 'currency': V; - }; 'SuggestMessageTimeDescription': { 'hint': V; 'duration': V; @@ -3035,6 +3049,9 @@ export interface LangPairPluralWithVariables { 'ActionGiftStarsTitle': { 'amount': V; }; + 'TonAmountText': { + 'amount': V; + }; 'ActionBoostApplyYou': { 'count': V; }; diff --git a/src/util/formatCurrency.tsx b/src/util/formatCurrency.tsx index 1184133ac..ab2f249f9 100644 --- a/src/util/formatCurrency.tsx +++ b/src/util/formatCurrency.tsx @@ -2,8 +2,24 @@ import { type TeactNode } from '../lib/teact/teact'; import type { LangFn } from './localization'; -import { STARS_CURRENCY_CODE } from '../config'; -import { formatStarsAsIcon } from './localization/format'; +import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../config'; +import { formatStarsAsIcon, formatTonAsIcon } from './localization/format'; + +export function convertCurrencyFromBaseUnit(amount: number, currency: string) { + return amount / 10 ** getCurrencyExp(currency); +} + +export function convertCurrencyToBaseUnit(amount: number, currency: string) { + return amount * 10 ** getCurrencyExp(currency); +} + +export function convertTonFromNanos(nanos: number): number { + return convertCurrencyFromBaseUnit(nanos, TON_CURRENCY_CODE); +} + +export function convertTonToNanos(ton: number): number { + return convertCurrencyToBaseUnit(ton, TON_CURRENCY_CODE); +} export function formatCurrency( lang: LangFn, @@ -15,15 +31,24 @@ export function formatCurrency( asFontIcon?: boolean; }, ): TeactNode { - const price = totalPrice / 10 ** getCurrencyExp(currency); + const price = convertCurrencyFromBaseUnit(totalPrice, currency); if (currency === STARS_CURRENCY_CODE) { return formatStarsAsIcon(lang, price, { asFont: options?.asFontIcon, className: options?.iconClassName }); } + if (currency === TON_CURRENCY_CODE) { + return formatTonAsIcon(lang, price, { asFont: options?.asFontIcon, className: options?.iconClassName }); + } + return formatCurrencyAsString(totalPrice, currency, lang.code, options); } +export function convertTonToUsd(amount: number, usdRate: number): number { + const tonInRegularUnits = convertTonFromNanos(amount); + return tonInRegularUnits * usdRate * 100; +} + export function formatCurrencyAsString( totalPrice: number, currency: string, @@ -32,7 +57,7 @@ export function formatCurrencyAsString( shouldOmitFractions?: boolean; }, ) { - const price = totalPrice / 10 ** getCurrencyExp(currency); + const price = convertCurrencyFromBaseUnit(totalPrice, currency); if ((options?.shouldOmitFractions || currency === STARS_CURRENCY_CODE) && Number.isInteger(price)) { return new Intl.NumberFormat(locale, { @@ -43,6 +68,15 @@ export function formatCurrencyAsString( }).format(price); } + if (currency === TON_CURRENCY_CODE) { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency, + minimumFractionDigits: 0, + maximumFractionDigits: 10, + }).format(price); + } + return new Intl.NumberFormat(locale, { style: 'currency', currency, @@ -50,7 +84,7 @@ export function formatCurrencyAsString( } function getCurrencyExp(currency: string) { - if (currency === 'TON') { + if (currency === TON_CURRENCY_CODE) { return 9; } if (currency === 'CLF') { diff --git a/src/util/localization/format.tsx b/src/util/localization/format.tsx index 55021375e..b882363d9 100644 --- a/src/util/localization/format.tsx +++ b/src/util/localization/format.tsx @@ -1,6 +1,7 @@ import type { LangFn } from './types'; import { STARS_ICON_PLACEHOLDER } from '../../config'; +import { convertCurrencyFromBaseUnit } from '../../util/formatCurrency'; import buildClassName from '../buildClassName'; import Icon from '../../components/common/icons/Icon'; @@ -10,6 +11,37 @@ export function formatStarsAsText(lang: LangFn, amount: number) { return lang('StarsAmountText', { amount }, { pluralValue: amount }); } +export function formatTonAsText(lang: LangFn, amount: number) { + return lang('TonAmountText', { amount: lang.preciseNumber(amount) }, { pluralValue: amount }); +} + +export function formatTonAsIcon(lang: LangFn, amount: number | string, options?: { + asFont?: boolean; className?: string; containerClassName?: string; shouldConvertFromNanos?: boolean; }) { + const { className, containerClassName, shouldConvertFromNanos } = options || {}; + const formattedAmount = shouldConvertFromNanos ? convertCurrencyFromBaseUnit(Number(amount), 'TON') : amount; + const icon = ; + + if (containerClassName) { + return ( + + {lang('TonAmount', { amount: formattedAmount }, { + withNodes: true, + specialReplacement: { + '💎': icon, + }, + })} + + ); + } + + return lang('TonAmount', { amount: formattedAmount }, { + withNodes: true, + specialReplacement: { + '💎': icon, + }, + }); +} + export function formatStarsAsIcon(lang: LangFn, amount: number | string, options?: { asFont?: boolean; className?: string; containerClassName?: string; }) { const { asFont, className, containerClassName } = options || {}; diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index 7d3d08891..6a92f6921 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -169,6 +169,10 @@ function createFormatters() { conjunction: createListFormat(langCode, 'conjunction'), disjunction: createListFormat(langCode, 'disjunction'), number: new Intl.NumberFormat(langCode), + preciseNumber: new Intl.NumberFormat(langCode, { + minimumFractionDigits: 0, + maximumFractionDigits: 10, + }), }; } catch (e) { // eslint-disable-next-line no-console @@ -179,6 +183,10 @@ function createFormatters() { conjunction: createListFormat(FORMATTERS_FALLBACK_LANG, 'conjunction'), disjunction: createListFormat(FORMATTERS_FALLBACK_LANG, 'disjunction'), number: new Intl.NumberFormat(FORMATTERS_FALLBACK_LANG), + preciseNumber: new Intl.NumberFormat(FORMATTERS_FALLBACK_LANG, { + minimumFractionDigits: 0, + maximumFractionDigits: 10, + }), }; } } @@ -325,6 +333,7 @@ function createTranslationFn(): LangFn { fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', '); fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', '); fn.number = (value: number) => formatters?.number.format(value) || String(value); + fn.preciseNumber = (value: number) => formatters?.preciseNumber.format(value) || String(value); fn.internalFormatters = formatters!; fn.languageInfo = language!; return fn; diff --git a/src/util/localization/types.ts b/src/util/localization/types.ts index 874a6f1db..44493eac1 100644 --- a/src/util/localization/types.ts +++ b/src/util/localization/types.ts @@ -155,6 +155,7 @@ export type LangFn = { conjunction: (list: string[]) => string; disjunction: (list: string[]) => string; number: (value: number) => string; + preciseNumber: (value: number) => string; internalFormatters: LangFormatters; isRtl?: boolean; rawCode: string; @@ -171,6 +172,7 @@ export type LangFormatters = { conjunction: ListFormat; disjunction: ListFormat; number: Intl.NumberFormat; + preciseNumber: Intl.NumberFormat; }; /* GUARDS */