Stars Transaction: Refactor modal (#4900)

This commit is contained in:
zubiden 2024-08-29 15:52:30 +02:00 committed by Alexander Zinchuk
parent a4b33eb43a
commit 8433012a88
32 changed files with 623 additions and 573 deletions

View File

@ -370,6 +370,7 @@ function buildAction(
let isGiveaway: boolean | undefined;
let isUnclaimed: boolean | undefined;
let pluralValue: number | undefined;
let transactionId: string | undefined;
let targetUserIds = 'users' in action
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
@ -509,6 +510,7 @@ function buildAction(
text = 'Notification.WebAppSentData';
translationValues.push(action.text);
} else if (action instanceof GramJs.MessageActionGiftPremium) {
type = 'giftPremium';
text = isOutgoing ? 'ActionGiftOutbound' : 'ActionGiftInbound';
if (isOutgoing) {
translationValues.push('%gift_payment_amount%');
@ -561,6 +563,7 @@ function buildAction(
text = 'BoostingGiveawayJustStarted';
translationValues.push('%action_origin%');
} else if (action instanceof GramJs.MessageActionGiftCode) {
type = 'giftCode';
text = isOutgoing ? 'ActionGiftOutbound' : 'BoostingReceivedGiftNoName';
slug = action.slug;
months = action.months;
@ -621,7 +624,8 @@ function buildAction(
translationValues.unshift('%action_origin%');
}
} else if (action instanceof GramJs.MessageActionGiftStars) {
text = isOutgoing ? 'ActionGiftOutbound' : 'BoostingReceivedGiftNoName';
type = 'giftStars';
text = isOutgoing ? 'ActionGiftOutbound' : targetPeerId ? 'ActionGiftInbound' : 'BoostingReceivedGiftNoName';
if (isOutgoing) {
translationValues.push('%gift_payment_amount%');
} else {
@ -629,10 +633,20 @@ function buildAction(
}
if (targetPeerId) {
targetUserIds.push(targetPeerId);
targetChatId = targetPeerId;
}
if (action.cryptoCurrency) {
giftCryptoInfo = {
currency: action.cryptoCurrency,
amount: action.cryptoAmount!.toJSNumber(),
};
}
currency = action.currency;
amount = action.amount.toJSNumber();
stars = action.stars.toJSNumber();
transactionId = action.transactionId;
} else {
text = 'ChatList.UnsupportedMessage';
}
@ -664,6 +678,7 @@ function buildAction(
isTopicAction,
isUnclaimed,
pluralValue,
transactionId,
};
}

View File

@ -61,7 +61,10 @@ export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): Ap
return {
type: 'stars',
currency,
botId: buildApiPeerId(botId, 'user'),
peer: {
type: 'peer',
id: buildApiPeerId(botId, 'user'),
},
date,
text,
title,
@ -426,7 +429,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
const {
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending,
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift,
} = transaction;
if (photo) {
@ -448,6 +451,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
hasFailed: failed,
isPending: pending,
messageId: msgId,
isGift: gift,
extendedMedia: boughtExtendedMedia,
};
}

View File

@ -102,7 +102,7 @@ export {
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions,
sendStarPaymentForm, getStarsGiftOptions,
sendStarPaymentForm, getStarsGiftOptions, fetchStarsTransactionById,
} from './payments';
export * from './fragment';

View File

@ -459,16 +459,19 @@ export async function fetchStarsStatus() {
}
export async function fetchStarsTransactions({
peer,
offset,
isInbound,
isOutbound,
}: {
peer?: ApiPeer;
offset?: string;
isInbound?: true;
isOutbound?: true;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({
peer: new GramJs.InputPeerSelf(),
peer: inputPeer,
offset,
inbound: isInbound,
outbound: isOutbound,
@ -490,6 +493,34 @@ export async function fetchStarsTransactions({
};
}
export async function fetchStarsTransactionById({
id, peer,
}: {
id: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactionsByID({
peer: inputPeer,
id: [new GramJs.InputStarsTransaction({
id,
})],
}));
if (!result?.history?.[0]) {
return undefined;
}
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
return {
users,
chats,
transaction: buildApiStarsTransaction(result.history[0]),
};
}
export async function fetchStarsTopupOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());

View File

@ -397,6 +397,9 @@ export interface ApiAction {
| 'joinedChannel'
| 'chatBoost'
| 'receipt'
| 'giftStars'
| 'giftPremium'
| 'giftCode'
| 'other';
photo?: ApiPhoto;
amount?: number;

View File

@ -3,7 +3,7 @@ import type { ApiInvoiceContainer } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiChat } from './chats';
import type {
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia, MediaContent,
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia,
} from './messages';
import type { PrepaidGiveaway, StatisticsOverviewPercentage } from './statistics';
import type { ApiUser } from './users';
@ -63,8 +63,7 @@ export interface ApiLabeledPrice {
export interface ApiReceiptStars {
type: 'stars';
botId?: string;
peer?: ApiStarsTransactionPeer;
peer: ApiStarsTransactionPeer;
date: number;
title?: string;
text?: string;
@ -273,18 +272,20 @@ export type ApiStarsTransactionPeer =
| ApiStarsTransactionPeerPeer;
export interface ApiStarsTransaction {
id: string;
id?: string;
peer: ApiStarsTransactionPeer;
messageId?: number;
stars: number;
isRefund?: true;
isGift?: true;
isMyGift?: true; // Used only for outgoing star gift messages
hasFailed?: true;
isPending?: true;
date: number;
title?: string;
description?: string;
photo?: ApiWebDocument;
extendedMedia?: MediaContent[];
extendedMedia?: BoughtPaidMedia[];
}
export interface ApiStarTopupOption {

View File

@ -25,10 +25,10 @@ export { default as PremiumLimitReachedModal } from '../components/main/premium/
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
export { default as StarGiftInfoModal } from '../components/modals/stars/StarGiftInfoModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';

View File

@ -26,6 +26,7 @@ import {
isUserId,
} from '../../global/helpers';
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import { getFirstLetters } from '../../util/textFormat';
import { getPeerColorClass } from './helpers/peerColor';
import renderText from './helpers/renderText';
@ -225,6 +226,7 @@ const Avatar: FC<OwnProps> = ({
const isRoundedRect = (isCustomPeer && peer.isAvatarSquare)
|| (isForum && !((withStory || withStorySolid) && realPeer?.hasStories));
const isPremiumGradient = isCustomPeer && peer.withPremiumGradient;
const customColor = isCustomPeer && peer.customPeerAvatarColor;
const fullClassName = buildClassName(
`Avatar size-${size}`,
@ -273,6 +275,7 @@ const Avatar: FC<OwnProps> = ({
data-peer-id={realPeer?.id}
data-test-sender-id={IS_TEST ? realPeer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined}
style={buildStyle(customColor && `--color-user: ${customColor}`)}
onClick={handleClick}
onMouseDown={handleMouseDown}
>

View File

@ -1,6 +1,5 @@
import '../../global/actions/all';
import type { FC } from '../../lib/teact/teact';
import React, {
memo, useEffect, useLayoutEffect,
useRef, useState,
@ -9,7 +8,6 @@ import { addExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiChat,
ApiChatFolder,
ApiMessage,
ApiUser,
@ -74,7 +72,6 @@ import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
import MiddleColumn from '../middle/MiddleColumn';
import ModalContainer from '../modals/ModalContainer';
import StarGiftInfoModal from '../modals/stars/StarGiftInfoModal';
import PaymentModal from '../payment/PaymentModal.async';
import ReceiptModal from '../payment/ReceiptModal.async';
import RightColumn from '../right/RightColumn';
@ -106,7 +103,6 @@ export interface OwnProps {
type StateProps = {
isMasterTab?: boolean;
chat?: ApiChat;
currentUserId?: string;
isLeftColumnOpen: boolean;
isMiddleColumnOpen: boolean;
@ -144,12 +140,10 @@ type StateProps = {
isPaymentModalOpen?: boolean;
isReceiptModalOpen?: boolean;
isReactionPickerOpen: boolean;
isAppendModalOpen?: boolean;
isGiveawayModalOpen?: boolean;
isDeleteMessageModalOpen?: boolean;
isPremiumGiftingPickerModal?: boolean;
isStarsGiftingPickerModal?: boolean;
isStarGiftInfoModal?: boolean;
isCurrentUserPremium?: boolean;
noRightColumnAnimation?: boolean;
withInterfaceAnimations?: boolean;
@ -162,7 +156,7 @@ const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
// eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_isLogged = false;
const Main: FC<OwnProps & StateProps> = ({
const Main = ({
isMobile,
isLeftColumnOpen,
isMiddleColumnOpen,
@ -201,7 +195,6 @@ const Main: FC<OwnProps & StateProps> = ({
isDeleteMessageModalOpen,
isPremiumGiftingPickerModal,
isStarsGiftingPickerModal,
isStarGiftInfoModal,
isPaymentModalOpen,
isReceiptModalOpen,
isReactionPickerOpen,
@ -211,7 +204,7 @@ const Main: FC<OwnProps & StateProps> = ({
noRightColumnAnimation,
isSynced,
currentUserId,
}) => {
}: OwnProps & StateProps) => {
const {
initMain,
loadAnimatedEmojis,
@ -584,7 +577,6 @@ const Main: FC<OwnProps & StateProps> = ({
{isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />}
{isPremiumGiftingPickerModal && <PremiumGiftingPickerModal isOpen={isPremiumGiftingPickerModal} />}
{isStarsGiftingPickerModal && <StarsGiftingPickerModal isOpen={isStarsGiftingPickerModal} />}
{isStarGiftInfoModal && <StarGiftInfoModal isOpen={isStarGiftInfoModal} />}
<PremiumLimitReachedModal limit={limitReached} />
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
@ -627,7 +619,6 @@ export default memo(withGlobal<OwnProps>(
deleteMessageModal,
giftingModal,
starsGiftingModal,
starGiftInfoModal,
isMasterTab,
payment,
limitReachedModal,
@ -685,7 +676,6 @@ export default memo(withGlobal<OwnProps>(
isDeleteMessageModalOpen: Boolean(deleteMessageModal),
isPremiumGiftingPickerModal: giftingModal?.isOpen,
isStarsGiftingPickerModal: starsGiftingModal?.isOpen,
isStarGiftInfoModal: starGiftInfoModal?.isOpen,
limitReached: limitReachedModal?.limit,
isPaymentModalOpen: payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(payment.receipt),

View File

@ -13,7 +13,7 @@ import type { FocusDirection, ThreadId } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import {
getChatTitle, getMessageHtmlId, getSenderTitle, isJoinedChannelMessage,
getChatTitle, getMessageHtmlId, isJoinedChannelMessage,
} from '../../global/helpers';
import { getMessageReplyInfo } from '../../global/helpers/replies';
import {
@ -35,7 +35,6 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useEnsureMessage from '../../hooks/useEnsureMessage';
import useFlag from '../../hooks/useFlag';
import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import useShowTransition from '../../hooks/useShowTransition';
import useFocusMessage from './message/hooks/useFocusMessage';
@ -104,11 +103,10 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onPinnedIntersectionChange,
}) => {
const {
openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarGiftInfoModal,
openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarsTransactionFromGift,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
@ -131,11 +129,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const noAppearanceAnimation = appearanceOrder <= 0;
const [isShown, markShown] = useFlag(noAppearanceAnimation);
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift'));
const isPremiumGift = message.content.action?.type === 'giftPremium';
const isGiftCode = message.content.action?.type === 'giftCode';
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
const isJoinedMessage = isJoinedChannelMessage(message);
const hasStars = Boolean(message.content.action?.stars);
const isStarsGift = message.content.action?.type === 'giftStars';
useEffect(() => {
if (noAppearanceAnimation) {
@ -149,7 +147,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const shouldShowConfettiRef = useRef((() => {
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
return isGift && !message.isOutgoing && isUnread;
return isPremiumGift && !message.isOutgoing && isUnread;
})());
useEffect(() => {
@ -201,10 +199,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
};
const handleStarGiftClick = () => {
openStarGiftInfoModal({
toUserId: targetUserIds?.[0],
stars: message.content.action!.stars,
date: message.date,
openStarsTransactionFromGift({
chatId: message.chatId,
messageId: message.id,
});
};
@ -245,10 +242,10 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
function renderGift() {
return (
<span
className="action-message-gift action-message-stars-gift"
className="action-message-gift"
tabIndex={0}
role="button"
onClick={hasStars ? handleStarGiftClick : handlePremiumGiftClick}
onClick={handlePremiumGiftClick}
>
<AnimatedIconFromSticker
key={message.id}
@ -257,11 +254,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
noLoop
nonInteractive
/>
<strong>{hasStars ? oldLang('Stars', message.content.action?.stars)
: oldLang('ActionGiftPremiumTitle')}
</strong>
<span>{hasStars ? oldLang('ActionGiftStarsSubtitleYou')
: oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
<strong>{oldLang('ActionGiftPremiumTitle')}</strong>
<span>
{oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
</span>
<span className="action-message-button">{oldLang('ActionGiftPremiumView')}</span>
@ -277,7 +272,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
className="action-message-gift action-message-gift-code"
tabIndex={0}
role="button"
onClick={hasStars ? handleStarGiftClick : handleGiftCodeClick}
onClick={handleGiftCodeClick}
>
<AnimatedIconFromSticker
key={message.id}
@ -286,34 +281,57 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
noLoop
nonInteractive
/>
<strong>{hasStars ? oldLang('Stars', message.content.action?.stars)
: oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
<strong>
{oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
</strong>
<span className="action-message-subtitle">
{hasStars ? lang('GiftStarsOutgoing', {
user: (
<b>
{senderUser && renderText(getSenderTitle(oldLang, senderUser) || '', ['simple_markdown'])}
</b>
),
}, {
withNodes: true,
}) : targetChat && renderText(oldLang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed
{targetChat && renderText(oldLang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed
? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize',
getChatTitle(oldLang, targetChat)),
['simple_markdown'])}
</span>
{!hasStars && (
<span className="action-message-subtitle">
{renderText(oldLang(
'BoostingUnclaimedPrizeDuration',
oldLang('Months', message.content.action?.months, 'i'),
), ['simple_markdown'])}
</span>
)}
<span className="action-message-subtitle">
{renderText(oldLang(
'BoostingUnclaimedPrizeDuration',
oldLang('Months', message.content.action?.months, 'i'),
), ['simple_markdown'])}
</span>
<span className="action-message-button">{
oldLang(hasStars ? 'ActionGiftPremiumView' : 'BoostingReceivedGiftOpenBtn')
oldLang('BoostingReceivedGiftOpenBtn')
}
</span>
</span>
);
}
function renderStarsGift() {
return (
<span
className="action-message-gift action-message-gift-code"
tabIndex={0}
role="button"
onClick={handleStarGiftClick}
>
<AnimatedIconFromSticker
key={message.id}
sticker={premiumGiftSticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
/>
<strong>
{oldLang('Stars', message.content.action!.stars)}
</strong>
<span className="action-message-subtitle">
{renderText(
oldLang(!message.isOutgoing
? 'ActionGiftStarsSubtitleYou' : 'ActionGiftStarsSubtitle', getChatTitle(oldLang, targetChat!)),
['simple_markdown'],
)}
</span>
<span className="action-message-button">{
oldLang('ActionGiftPremiumView')
}
</span>
</span>
@ -323,7 +341,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const className = buildClassName(
'ActionMessage message-list-item',
isFocused && !noFocusHighlight && 'focused',
(isGift || isSuggestedAvatar) && 'centered-action',
(isPremiumGift || isSuggestedAvatar) && 'centered-action',
isContextMenuShown && 'has-menu-open',
isLastInList && 'last-in-list',
transitionClassNames,
@ -342,8 +360,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && (
<span className="action-message-content" onClick={handleClick}>{renderContent()}</span>
)}
{isGift && renderGift()}
{isPremiumGift && renderGift()}
{isGiftCode && renderGiftCode()}
{isStarsGift && renderStarsGift()}
{isSuggestedAvatar && (
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
)}

View File

@ -17,6 +17,7 @@ import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async';
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
import WebAppModal from './webApp/WebAppModal.async';
@ -34,7 +35,8 @@ type ModalKey = keyof Pick<TabState,
'reportAdModal' |
'starsBalanceModal' |
'isStarPaymentModalOpen' |
'webApp'
'webApp' |
'starsTransactionModal'
>;
type StateProps = {
@ -63,6 +65,7 @@ const MODALS: ModalRegistry = {
mapModal: MapModal,
isStarPaymentModalOpen: StarsPaymentModal,
starsBalanceModal: StarsBalanceModal,
starsTransactionModal: StarsTransactionInfoModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -1,18 +0,0 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './StarGiftInfoModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const StarGiftInfoModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const StarGiftInfoModal = useModuleLoader(Bundles.Extra, 'StarGiftInfoModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return StarGiftInfoModal ? <StarGiftInfoModal {...props} /> : undefined;
};
export default StarGiftInfoModalAsync;

View File

@ -1,32 +0,0 @@
@use '../../../styles/mixins';
.centered {
text-align: center !important;
}
.section {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
position: relative;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.info {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0.5rem;
}
.starTitle {
margin: 0;
}
.footer {
margin: 1rem 0;
color: var(--color-text-secondary);
}

View File

@ -1,150 +0,0 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiPeer } from '../../../api/types';
import { getSenderTitle } from '../../../global/helpers';
import { selectTabState, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
import SafeLink from '../../common/SafeLink';
import TableInfoModal, { type TableData } from '../common/TableInfoModal';
import styles from './StarGiftInfoModal.module.scss';
import StarLogo from '../../../assets/icons/StarLogo.svg';
import StarsBackground from '../../../assets/stars-bg.png';
export type OwnProps = {
isOpen?: boolean;
};
export type StateProps = {
stars?: number;
user?: ApiPeer;
date?: number;
};
const StarGiftInfoModal = ({
isOpen,
stars,
user,
date,
}: OwnProps & StateProps) => {
const {
closeStarGiftInfoModal,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
const infoText = useMemo(() => {
const linkText = oldLang('GiftStarsSubtitleLinkName');
return lang('CreditsBoxHistoryEntryGiftOutAbout',
{
user: (
<b>
{user && renderText(getSenderTitle(oldLang, user) || '', ['simple_markdown'])}
</b>
),
link: (
<SafeLink
url={oldLang('lng_paid_about_link_url')}
text={linkText}
/>
),
},
{
withNodes: true,
});
}, [lang, oldLang, user]);
const footerText = useMemo(() => {
const linkText = oldLang('lng_payments_terms_link');
return lang('CreditsBoxOutAbout', {
link: (
<SafeLink
url={oldLang('StarsTOSLink')}
text={linkText}
/>
),
}, {
withNodes: true,
});
}, [lang, oldLang]);
const handleButtonClick = useLastCallback(() => {
closeStarGiftInfoModal();
});
const modalData = useMemo(() => {
if (!isOpen) return undefined;
const header = (
<>
<h2 className={buildClassName(styles.starTitle, styles.centered)}>{oldLang('StarsGiftSent')}</h2>
<div className={styles.info}>
<p className={buildClassName(styles.starTitle, styles.centered)}>{stars}</p>
<StarIcon type="gold" size="middle" />
</div>
<p className={styles.centered}>{infoText}</p>
</>
);
const tableData = [
[oldLang('Recipient'), user ? { chatId: user.id } : oldLang('BoostingNoRecipient')],
[oldLang('BoostingDate'), formatDateTimeToString(date! * 1000, lang.code, true)],
] satisfies TableData;
const footer = (
<span className={buildClassName(styles.footer, styles.centered)}>
{footerText}
</span>
);
return {
header,
tableData,
footer,
};
}, [isOpen, oldLang, stars, infoText, user, date, lang.code, footerText]);
if (!modalData) return undefined;
return (
<TableInfoModal
isOpen={isOpen}
headerImageUrl={StarLogo}
logoBackground={StarsBackground}
tableData={modalData.tableData}
header={modalData.header}
footer={modalData.footer}
buttonText={lang('Close')}
onButtonClick={handleButtonClick}
onClose={closeStarGiftInfoModal}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
starGiftInfoModal,
} = selectTabState(global);
const toUserId = starGiftInfoModal?.toUserId;
const user = toUserId ? selectUser(global, toUserId) : undefined;
return {
stars: starGiftInfoModal?.stars,
user,
date: starGiftInfoModal?.date,
};
},
)(StarGiftInfoModal));

View File

@ -24,8 +24,8 @@ import Modal from '../../ui/Modal';
import TabList, { type TabWithProperties } from '../../ui/TabList';
import Transition from '../../ui/Transition';
import BalanceBlock from './BalanceBlock';
import TransactionItem from './StarsTransactionItem';
import StarTopupOptionList from './StarTopupOptionList';
import TransactionItem from './transaction/StarsTransactionItem';
import styles from './StarsBalanceModal.module.scss';

View File

@ -22,7 +22,7 @@ import StarIcon from '../../common/icons/StarIcon';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import BalanceBlock from './BalanceBlock';
import PaidMediaThumb from './PaidMediaThumb';
import PaidMediaThumb from './transaction/PaidMediaThumb';
import styles from './StarsBalanceModal.module.scss';

View File

@ -1,14 +1,14 @@
import React, { memo } from '../../../lib/teact/teact';
import React, { memo } from '../../../../lib/teact/teact';
import type { ApiMediaExtendedPreview, BoughtPaidMedia } from '../../../api/types';
import type { ApiMediaExtendedPreview, BoughtPaidMedia } from '../../../../api/types';
import { getMediaHash, getMediaThumbUri } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { getMediaHash, getMediaThumbUri } from '../../../../global/helpers';
import buildClassName from '../../../../util/buildClassName';
import useMedia from '../../../hooks/useMedia';
import useMedia from '../../../../hooks/useMedia';
import Icon from '../../common/icons/Icon';
import MediaSpoiler from '../../common/MediaSpoiler';
import Icon from '../../../common/icons/Icon';
import MediaSpoiler from '../../../common/MediaSpoiler';
import styles from './PaidMediaThumb.module.scss';

View File

@ -1,26 +1,26 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import React, { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
import type {
ApiPeer,
ApiStarsTransaction,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { CustomPeer } from '../../../types';
} from '../../../../api/types';
import type { GlobalState } from '../../../../global/types';
import type { CustomPeer } from '../../../../types';
import { getSenderTitle } from '../../../global/helpers';
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../global/helpers/payments';
import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer';
import { getSenderTitle } from '../../../../global/helpers';
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments';
import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { CUSTOM_PEER_PREMIUM } from '../../../../util/objects/customPeer';
import useSelector from '../../../hooks/data/useSelector';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useSelector from '../../../../hooks/data/useSelector';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import StarIcon from '../../common/icons/StarIcon';
import Avatar from '../../../common/Avatar';
import StarIcon from '../../../common/icons/StarIcon';
import PaidMediaThumb from './PaidMediaThumb';
import styles from './StarsTransactionItem.module.scss';
@ -36,7 +36,7 @@ function selectOptionalPeer(peerId?: string) {
}
const StarsTransactionItem = ({ transaction }: OwnProps) => {
const { getStarsReceipt } = getActions();
const { openStarsTransactionModal } = getActions();
const {
date,
stars,
@ -90,7 +90,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
}, [lang, peer, transaction]);
const handleClick = useLastCallback(() => {
getStarsReceipt({ transaction });
openStarsTransactionModal({ transaction });
});
return (

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../../lib/teact/teact';
import React from '../../../../lib/teact/teact';
import type { OwnProps } from './StarsTransactionModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const StarsTransactionModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const StarsTransactionModal = useModuleLoader(Bundles.Extra, 'StarsTransactionInfoModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return StarsTransactionModal ? <StarsTransactionModal {...props} /> : undefined;
};
export default StarsTransactionModalAsync;

View File

@ -0,0 +1,69 @@
.modal {
z-index: calc(var(--z-media-viewer) - 1);
}
.positive {
color: var(--color-success);
}
.negative {
color: var(--color-error);
}
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
position: relative;
}
.amount {
display: flex;
gap: 0.25rem;
font-size: 1.25rem;
font-weight: 500;
line-height: 1.325;
}
.title, .description, .amount {
margin-bottom: 0;
}
.tid {
font-family: var(--font-family-monospace);
font-size: 0.875rem;
cursor: pointer;
}
.description {
text-align: center;
}
.footer {
text-align: center;
margin-block: 0.5rem;
}
.starsBackground {
position: absolute;
height: 8rem;
top: -8.5rem;
left: 50%;
transform: translateX(-50%);
}
.mediaShift {
top: -1.5rem;
}
.copyIcon {
margin-inline-start: 0.25rem;
color: var(--color-primary);
}
.mediaPreview {
margin-bottom: 2rem;
cursor: var(--custom-cursor, pointer);
}

View File

@ -0,0 +1,213 @@
import type { FC } from '../../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type {
ApiPeer,
ApiStarsTransactionPeer,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { MediaViewerOrigin } from '../../../../types';
import { getMessageLink } from '../../../../global/helpers';
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments';
import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import usePrevious from '../../../../hooks/usePrevious';
import Icon from '../../../common/icons/Icon';
import StarIcon from '../../../common/icons/StarIcon';
import SafeLink from '../../../common/SafeLink';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import PaidMediaThumb from './PaidMediaThumb';
import styles from './StarsTransactionModal.module.scss';
import StarsBackground from '../../../../assets/stars-bg.png';
export type OwnProps = {
modal: TabState['starsTransactionModal'];
};
type StateProps = {
peer?: ApiPeer;
};
const StarsTransactionModal: FC<OwnProps & StateProps> = ({
modal, peer,
}) => {
const { showNotification, openMediaViewer, closeStarsTransactionModal } = getActions();
const lang = useOldLang();
const { transaction } = modal || {};
const handleOpenMedia = useLastCallback(() => {
const media = transaction?.extendedMedia;
if (!media) return;
openMediaViewer({
origin: MediaViewerOrigin.StarsTransaction,
standaloneMedia: media.flatMap((item) => Object.values(item)),
});
});
const starModalData = useMemo(() => {
if (!transaction) {
return undefined;
}
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
const toName = transaction.peer && lang(getStarsPeerTitleKey(transaction.peer));
const title = transaction.title || (customPeer ? lang(customPeer.titleKey) : undefined);
const messageLink = peer && transaction.messageId
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
const media = transaction.extendedMedia;
const mediaAmount = media?.length || 0;
const areAllPhotos = media?.every((m) => !m.video);
const areAllVideos = media?.every((m) => !m.photo);
const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', mediaAmount)
: areAllVideos ? lang('Stars.Transfer.Videos', mediaAmount)
: lang('Media', mediaAmount);
const description = transaction.description || (media ? mediaText : undefined);
const header = (
<div className={styles.header}>
{media && (
<PaidMediaThumb
className={buildClassName(styles.mediaPreview, 'transaction-media-preview')}
media={media}
onClick={handleOpenMedia}
/>
)}
<img
className={buildClassName(styles.starsBackground, media && styles.mediaShift)}
src={StarsBackground}
alt=""
draggable={false}
/>
{title && <h1 className={styles.title}>{title}</h1>}
<p className={styles.description}>{description}</p>
<p className={styles.amount}>
<span className={buildClassName(styles.amount, transaction.stars < 0 ? styles.negative : styles.positive)}>
{formatStarsTransactionAmount(transaction.stars)}
</span>
<StarIcon type="gold" size="big" />
</p>
</div>
);
const tableData: TableData = [];
tableData.push([
lang(transaction.stars < 0 || transaction.isMyGift ? 'Stars.Transaction.To'
: peerId ? 'Star.Transaction.From' : 'Stars.Transaction.Via'),
peerId ? { chatId: peerId } : toName || '',
]);
if (messageLink) {
tableData.push([lang('Stars.Transaction.Media'), <SafeLink url={messageLink} text={messageLink} />]);
}
if (transaction.id) {
tableData.push([
lang('Stars.Transaction.Id'),
(
<span
className={styles.tid}
onClick={() => {
copyTextToClipboard(transaction.id!);
showNotification({
message: lang('StarsTransactionIDCopied'),
});
}}
>
{transaction.id}
<Icon className={styles.copyIcon} name="copy" />
</span>
),
]);
}
tableData.push([
lang('Stars.Transaction.Date'),
formatDateTimeToString(transaction.date * 1000, lang.code, true),
]);
const footerText = lang('lng_credits_box_out_about');
const footerTextParts = footerText.split('{link}');
const footer = (
<span className={styles.footer}>
{footerTextParts[0]}
<SafeLink url={lang('StarsTOSLink')} text={lang('lng_credits_summary_options_about_link')} />
{footerTextParts[1]}
</span>
);
return {
header,
tableData,
footer,
avatarPeer: !transaction.photo ? (peer || customPeer) : undefined,
};
}, [lang, transaction, peer]);
const prevModalData = usePrevious(starModalData);
const renderingModalData = prevModalData || starModalData;
return (
<TableInfoModal
isOpen={Boolean(transaction)}
className={styles.modal}
header={renderingModalData?.header}
tableData={renderingModalData?.tableData}
footer={renderingModalData?.footer}
noHeaderImage={Boolean(transaction?.extendedMedia)}
headerAvatarWebPhoto={transaction?.photo}
headerAvatarPeer={renderingModalData?.avatarPeer}
buttonText={lang('OK')}
onClose={closeStarsTransactionModal}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const peerId = modal?.transaction?.peer?.type === 'peer' && modal.transaction.peer.id;
const peer = peerId ? selectPeer(global, peerId) : undefined;
return {
peer,
};
},
)(StarsTransactionModal));
function getStarsPeerTitleKey(peer: ApiStarsTransactionPeer) {
switch (peer.type) {
case 'appStore':
return 'AppStore';
case 'playMarket':
return 'PlayMarket';
case 'fragment':
return 'Fragment';
case 'premiumBot':
return 'StarsTransactionBot';
case 'ads':
return 'StarsTransactionAds';
default:
return 'Stars.Transaction.Unsupported.Title';
}
}

View File

@ -1,211 +1,60 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import { withGlobal } from '../../global';
import type {
ApiInvoice,
ApiPeer,
ApiReceipt, ApiReceiptRegular, ApiShippingAddress,
ApiStarsTransactionPeer,
} from '../../api/types';
import { MediaViewerOrigin } from '../../types';
import type { ApiInvoice, ApiShippingAddress, ApiWebDocument } from '../../api/types';
import type { Price } from '../../types';
import { getMessageLink } from '../../global/helpers';
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../global/helpers/payments';
import { selectPeer, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard';
import { formatDateTimeToString } from '../../util/dates/dateFormat';
import { selectTabState } from '../../global/selectors';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import useLang from '../../hooks/useLang';
import Icon from '../common/icons/Icon';
import StarIcon from '../common/icons/StarIcon';
import SafeLink from '../common/SafeLink';
import TableInfoModal, { type TableData } from '../modals/common/TableInfoModal';
import PaidMediaThumb from '../modals/stars/PaidMediaThumb';
import Button from '../ui/Button';
import Modal from '../ui/Modal';
import Checkout from './Checkout';
import './PaymentModal.scss';
import styles from './ReceiptModal.module.scss';
import StarsBackground from '../../assets/stars-bg.png';
export type OwnProps = {
isOpen?: boolean;
onClose: NoneToVoidFunction;
onClose: () => void;
};
type StateProps = {
receipt?: ApiReceipt;
peer?: ApiPeer;
prices?: Price[];
shippingPrices: any;
tipAmount?: number;
totalAmount?: number;
currency?: string;
info?: {
shippingAddress?: ApiShippingAddress;
phone?: string;
name?: string;
};
photo?: ApiWebDocument;
text?: string;
title?: string;
credentialsTitle?: string;
shippingMethod?: string;
};
const ReceiptModal: FC<OwnProps & StateProps> = ({
isOpen, receipt, peer, onClose,
isOpen,
onClose,
prices,
shippingPrices,
tipAmount,
totalAmount,
currency,
info,
photo,
text,
title,
credentialsTitle,
shippingMethod,
}) => {
const { showNotification, openMediaViewer } = getActions();
const lang = useOldLang();
const handleOpenMedia = useLastCallback(() => {
const media = receipt?.type === 'stars' && receipt.media;
if (!media) return;
openMediaViewer({
origin: MediaViewerOrigin.StarsTransaction,
standaloneMedia: media.flatMap((item) => Object.values(item)),
});
});
const starModalData = useMemo(() => {
if (receipt?.type !== 'stars') return undefined;
const customPeer = (receipt.peer && receipt.peer.type !== 'peer' && buildStarsTransactionCustomPeer(receipt.peer))
|| undefined;
const botId = receipt.botId || (receipt.peer?.type === 'peer' ? receipt.peer.id : undefined);
const toName = receipt.peer && lang(getStarsPeerTitleKey(receipt.peer));
const title = receipt.title || (customPeer ? lang(customPeer.titleKey) : undefined);
const messageLink = peer && receipt.messageId ? getMessageLink(peer, undefined, receipt.messageId) : undefined;
const media = receipt.media;
const mediaAmount = media?.length || 0;
const areAllPhotos = media?.every((m) => !m.video);
const areAllVideos = media?.every((m) => !m.photo);
const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', mediaAmount)
: areAllVideos ? lang('Stars.Transfer.Videos', mediaAmount)
: lang('Media', mediaAmount);
const description = receipt.text || (media ? mediaText : undefined);
const header = (
<div className={styles.header}>
{media && (
<PaidMediaThumb
className={buildClassName(styles.mediaPreview, 'transaction-media-preview')}
media={media}
onClick={handleOpenMedia}
/>
)}
<img
className={buildClassName(styles.starsBackground, receipt.media && styles.mediaShift)}
src={StarsBackground}
alt=""
draggable={false}
/>
{title && <h1 className={styles.title}>{title}</h1>}
<p className={styles.description}>{description}</p>
<p className={styles.amount}>
<span className={buildClassName(styles.amount, receipt.totalAmount < 0 ? styles.negative : styles.positive)}>
{formatStarsTransactionAmount(receipt.totalAmount)}
</span>
<StarIcon type="gold" size="big" />
</p>
</div>
);
const tableData: TableData = [];
tableData.push([
lang(receipt.totalAmount < 0 ? 'Stars.Transaction.To' : 'Stars.Transaction.Via'),
botId ? { chatId: botId } : toName || '',
]);
if (messageLink) {
tableData.push([lang('Stars.Transaction.Media'), <SafeLink url={messageLink} text={messageLink} />]);
}
tableData.push([
lang('Stars.Transaction.Id'),
(
<span
className={styles.tid}
onClick={() => {
copyTextToClipboard(receipt.transactionId);
showNotification({
message: lang('StarsTransactionIDCopied'),
});
}}
>
{receipt.transactionId}
<Icon className={styles.copyIcon} name="copy" />
</span>
),
]);
tableData.push([
lang('Stars.Transaction.Date'),
formatDateTimeToString(receipt.date * 1000, lang.code, true),
]);
const footerText = lang('lng_credits_box_out_about');
const footerTextParts = footerText.split('{link}');
const footer = (
<span className={styles.footer}>
{footerTextParts[0]}
<SafeLink url={lang('StarsTOSLink')} text={lang('lng_credits_summary_options_about_link')} />
{footerTextParts[1]}
</span>
);
return {
header,
tableData,
footer,
avatarPeer: !receipt.photo ? (peer || customPeer) : undefined,
};
}, [lang, receipt, peer]);
if (receipt?.type === 'regular') {
return <ReceiptModalRegular isOpen={isOpen} receipt={receipt} onClose={onClose} />;
}
return (
<TableInfoModal
isOpen={isOpen}
className={styles.modal}
header={starModalData?.header}
tableData={starModalData?.tableData}
footer={starModalData?.footer}
noHeaderImage={Boolean(receipt?.media)}
headerAvatarWebPhoto={receipt?.photo}
headerAvatarPeer={starModalData?.avatarPeer}
buttonText={lang('OK')}
onClose={onClose}
/>
);
};
function ReceiptModalRegular({
isOpen, receipt, onClose,
}: {
isOpen?: boolean;
receipt: ApiReceiptRegular;
onClose: NoneToVoidFunction;
}) {
const {
credentialsTitle,
currency,
prices,
tipAmount,
totalAmount,
info,
photo,
shippingMethod,
shippingPrices,
text,
title,
} = receipt;
const lang = useOldLang();
const lang = useLang();
const [isModalOpen, openModal, closeModal] = useFlag();
@ -265,18 +114,37 @@ function ReceiptModalRegular({
</div>
</Modal>
);
}
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { receipt } = selectTabState(global).payment;
const peerId = receipt?.type === 'stars' && (receipt.botId || (receipt.peer?.type === 'peer' && receipt.peer.id));
const peer = peerId ? selectPeer(global, peerId) : undefined;
const {
currency,
prices,
info,
totalAmount,
credentialsTitle,
shippingPrices,
shippingMethod,
photo,
text,
title,
tipAmount,
} = (receipt || {});
return {
receipt,
peer,
currency,
prices,
info,
tipAmount,
totalAmount,
credentialsTitle,
shippingPrices,
shippingMethod,
photo,
text,
title,
};
},
)(ReceiptModal));
@ -304,20 +172,3 @@ function getCheckoutInfo(paymentMethod?: string,
shippingMethod,
};
}
function getStarsPeerTitleKey(peer: ApiStarsTransactionPeer) {
switch (peer.type) {
case 'appStore':
return 'AppStore';
case 'playMarket':
return 'PlayMarket';
case 'fragment':
return 'Fragment';
case 'premiumBot':
return 'StarsTransactionBot';
case 'ads':
return 'StarsTransactionAds';
default:
return 'Stars.Transaction.Unsupported.Title';
}
}

View File

@ -12,11 +12,13 @@ import { buildQueryString } from '../../../util/requestQuery';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import { isChatChannel, isChatSuperGroup } from '../../helpers';
import { getRequestInputInvoice } from '../../helpers/payments';
import { getRequestInputInvoice, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
addChats,
addUsers, appendStarsTransactions, closeInvoice,
openStarsTransactionFromReceipt,
openStarsTransactionModal,
setInvoiceInfo, setPaymentForm,
setPaymentStep,
setReceipt,
@ -24,7 +26,6 @@ import {
setSmartGlocalCardInfo, setStripeCardInfo,
updateChatFullInfo,
updatePayment,
updateReceiptFromStarsTransaction,
updateShippingOptions,
updateStarsBalance,
} from '../../reducers';
@ -32,6 +33,7 @@ import { updateTabState } from '../../reducers/tabs';
import {
selectChat,
selectChatFullInfo,
selectChatMessage,
selectPaymentFormId,
selectPaymentInputInvoice, selectPaymentRequestId,
selectProviderPublicToken,
@ -132,15 +134,14 @@ addActionHandler('getReceipt', async (global, actions, payload): Promise<void> =
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = setReceipt(global, result.receipt, tabId);
if (result.receipt.type === 'stars') {
global = openStarsTransactionFromReceipt(global, result.receipt, tabId);
} else {
global = setReceipt(global, result.receipt, tabId);
}
setGlobal(global);
});
addActionHandler('getStarsReceipt', (global, actions, payload): ActionReturnType => {
const { transaction, tabId = getCurrentTabId() } = payload;
return updateReceiptFromStarsTransaction(global, transaction, tabId);
});
addActionHandler('clearPaymentError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, {
@ -534,37 +535,20 @@ addActionHandler('closeStarsGiftingModal', (global, actions, payload): ActionRet
}, tabId);
});
addActionHandler('openStarGiftInfoModal', (global, actions, payload): ActionReturnType => {
addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => {
const {
toUserId,
stars,
date,
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
if (!stars || !toUserId || !date) {
return;
}
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
global = getGlobal();
const transaction = getStarsTransactionFromGift(message);
if (!transaction) return undefined;
global = updateTabState(global, {
starGiftInfoModal: {
toUserId,
stars,
date,
isOpen: true,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeStarGiftInfoModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starGiftInfoModal: undefined,
}, tabId);
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openPremiumGiftModal', async (global, actions, payload): Promise<void> => {

View File

@ -2,7 +2,9 @@ import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { addActionHandler } from '../../index';
import { clearPayment, closeInvoice, updatePayment } from '../../reducers';
import {
clearPayment, closeInvoice, openStarsTransactionModal, updatePayment,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectTabState } from '../../selectors';
@ -72,3 +74,16 @@ addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionRet
starsBalanceModal: undefined,
}, tabId);
});
addActionHandler('openStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { transaction, tabId = getCurrentTabId() } = payload;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsTransactionModal: undefined,
}, tabId);
});

View File

@ -1,5 +1,10 @@
import type {
ApiInputInvoice, ApiRequestInputInvoice, ApiStarsTransactionPeer, ApiStarsTransactionPeerPeer,
ApiInputInvoice,
ApiMessage,
ApiRequestInputInvoice,
ApiStarsTransaction,
ApiStarsTransactionPeer,
ApiStarsTransactionPeerPeer,
} from '../../api/types';
import type { CustomPeer } from '../../types';
import type { GlobalState } from '../types';
@ -141,7 +146,7 @@ export function buildStarsTransactionCustomPeer(
isCustomPeer: true,
titleKey: 'Stars.Intro.Transaction.FragmentTopUp.Title',
subtitleKey: 'Stars.Intro.Transaction.FragmentTopUp.Subtitle',
peerColorId: -1, // Defaults to black
customPeerAvatarColor: '#000000',
};
}
@ -182,3 +187,23 @@ export function formatStarsTransactionAmount(amount: number) {
return `+ ${formatInteger(amount)}`;
}
export function getStarsTransactionFromGift(message: ApiMessage): ApiStarsTransaction | undefined {
const { action } = message.content;
if (action?.type !== 'giftStars') return undefined;
const { transactionId, stars } = action;
return {
id: transactionId!,
stars: stars!,
peer: {
type: 'peer',
id: message.isOutgoing ? message.chatId : message.senderId!,
},
date: message.date,
isGift: true,
isMyGift: message.isOutgoing || undefined,
};
}

View File

@ -1,5 +1,6 @@
import type {
ApiInvoice, ApiPaymentForm, ApiReceipt,
ApiInvoice, ApiPaymentForm,
ApiReceiptRegular,
ApiReceiptStars,
ApiStarsTransaction,
} from '../../api/types';
@ -8,7 +9,6 @@ import type {
GlobalState, StarsTransactionType, TabArgs, TabState,
} from '../types';
import { STARS_CURRENCY_CODE } from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { selectTabState } from '../selectors';
import { updateTabState } from './tabs';
@ -113,7 +113,7 @@ export function setConfirmPaymentUrl<T extends GlobalState>(
export function setReceipt<T extends GlobalState>(
global: T,
receipt?: ApiReceipt,
receipt?: ApiReceiptRegular,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
if (!receipt) {
@ -187,22 +187,30 @@ export function appendStarsTransactions<T extends GlobalState>(
};
}
export function updateReceiptFromStarsTransaction<T extends GlobalState>(
export function openStarsTransactionModal<T extends GlobalState>(
global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const receipt: ApiReceiptStars = {
type: 'stars',
totalAmount: transaction.stars,
currency: STARS_CURRENCY_CODE,
peer: transaction.peer,
date: transaction.date,
text: transaction.description,
title: transaction.title,
transactionId: transaction.id,
photo: transaction.photo,
media: transaction.extendedMedia,
messageId: transaction.messageId,
return updateTabState(global, {
starsTransactionModal: {
transaction,
},
}, tabId);
}
export function openStarsTransactionFromReceipt<T extends GlobalState>(
global: T, receipt: ApiReceiptStars, ...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const transaction: ApiStarsTransaction = {
id: receipt.transactionId,
peer: receipt.peer,
stars: receipt.totalAmount,
date: receipt.date,
title: receipt.title,
description: receipt.text,
photo: receipt.photo,
extendedMedia: receipt.media,
messageId: receipt.messageId,
};
return updatePayment(global, { receipt }, tabId);
return openStarsTransactionModal(global, transaction, tabId);
}

View File

@ -56,7 +56,7 @@ import type {
ApiQuickReply,
ApiReaction,
ApiReactionKey,
ApiReceipt,
ApiReceiptRegular,
ApiReportReason,
ApiSavedReactionTag,
ApiSendMessageAction,
@ -547,7 +547,7 @@ export type TabState = {
};
passwordMissing?: boolean;
savedCredentials?: ApiPaymentCredentials[];
receipt?: ApiReceipt;
receipt?: ApiReceiptRegular;
error?: {
field?: string;
message?: string;
@ -720,13 +720,6 @@ export type TabState = {
isOpen?: boolean;
};
starGiftInfoModal?: {
isOpen?: boolean;
toUserId: string;
date: number;
stars: number;
};
starsGiftModal?: {
isCompleted?: boolean;
isOpen?: boolean;
@ -734,6 +727,10 @@ export type TabState = {
starsGiftOptions?: ApiStarsGiftOption[];
};
starsTransactionModal?: {
transaction: ApiStarsTransaction;
};
giftModal?: {
isCompleted?: boolean;
isOpen?: boolean;
@ -1770,9 +1767,14 @@ export interface ActionPayloads {
chatId: string;
messageId: number;
} & WithTabId;
getStarsReceipt: {
openStarsTransactionModal: {
transaction: ApiStarsTransaction;
} & WithTabId;
openStarsTransactionFromGift: {
chatId: string;
messageId: number;
} & WithTabId;
closeStarsTransactionModal: WithTabId | undefined;
sendCredentialsInfo: {
credentials: ApiCredentials;
} & WithTabId;
@ -3241,13 +3243,6 @@ export interface ActionPayloads {
} & WithTabId) | undefined;
closeStarsGiftModal: WithTabId | undefined;
openStarGiftInfoModal: ({
toUserId?: string;
stars?: number;
date?: number;
} & WithTabId) | undefined;
closeStarGiftInfoModal: WithTabId | undefined;
setEmojiStatus: {
emojiStatus: ApiSticker;
expires?: number;

View File

@ -1633,6 +1633,7 @@ payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus;
payments.getStarsTransactions#97938d5a flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult;
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
@ -1698,4 +1699,4 @@ premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:
premium.getMyBoosts#be77b4a = premium.MyBoosts;
premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;
premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`;

View File

@ -361,6 +361,7 @@
"payments.getStarsTopupOptions",
"payments.getStarsStatus",
"payments.getStarsTransactions",
"payments.getStarsTransactionsByID",
"payments.sendStarsForm",
"payments.getStarsGiftOptions",
"payments.refundStarsCharge",

View File

@ -520,6 +520,7 @@ export interface CustomPeer {
avatarIcon: IconName;
isAvatarSquare?: boolean;
peerColorId?: number;
customPeerAvatarColor?: string;
withPremiumGradient?: boolean;
}