Support gifting premium for stars.

This commit is contained in:
John Preston 2025-03-04 14:13:39 +04:00
parent bd70a05861
commit 7d2878d81c
26 changed files with 321 additions and 112 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

View file

@ -3329,6 +3329,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}"; "lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
"lng_gift_premium_features" = "See Features >"; "lng_gift_premium_features" = "See Features >";
"lng_gift_premium_label" = "Premium"; "lng_gift_premium_label" = "Premium";
"lng_gift_premium_by_stars" = "or {amount}";
"lng_gift_stars_subtitle" = "Gift Stars"; "lng_gift_stars_subtitle" = "Gift Stars";
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}"; "lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
"lng_gift_stars_link" = "What are Stars >"; "lng_gift_stars_link" = "What are Stars >";
@ -3340,6 +3341,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_title" = "Send a Gift"; "lng_gift_send_title" = "Send a Gift";
"lng_gift_send_message" = "Enter Message"; "lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name"; "lng_gift_send_anonymous" = "Hide My Name";
"lng_gift_send_pay_with_stars" = "Pay with {amount}";
"lng_gift_send_stars_balance" = "Your balance is {amount}. {link}";
"lng_gift_send_stars_balance_link" = "Get More Stars >";
"lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile."; "lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile.";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message."; "lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
"lng_gift_send_anonymous_about_paid" = "You can hide your name from visitors to {user}'s profile. {recipient} will still see your name."; "lng_gift_send_anonymous_about_paid" = "You can hide your name from visitors to {user}'s profile. {recipient} will still see your name.";

View file

@ -479,6 +479,9 @@ rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::request() {
for (const auto &tlOption : result.v) { for (const auto &tlOption : result.v) {
const auto &data = tlOption.data(); const auto &data = tlOption.data();
tlMapOptions[data.vusers().v].push_back(tlOption); tlMapOptions[data.vusers().v].push_back(tlOption);
if (qs(data.vcurrency()) == Ui::kCreditsCurrency) {
continue;
}
const auto token = Token{ data.vusers().v, data.vmonths().v }; const auto token = Token{ data.vusers().v, data.vmonths().v };
_stores[token] = Store{ _stores[token] = Store{

View file

@ -30,11 +30,6 @@ ShortInfoBox {
labeledOneLine: FlatLabel; labeledOneLine: FlatLabel;
} }
boxStarIconEmoji: IconEmoji {
icon: icon{{ "payments/small_star", windowFg }};
padding: margins(0px, -2px, 0px, 0px);
}
countryRowHeight: 36px; countryRowHeight: 36px;
countryRowNameFont: semiboldFont; countryRowNameFont: semiboldFont;
countryRowNameFg: boxTextFg; countryRowNameFg: boxTextFg;

View file

@ -512,7 +512,7 @@ TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) { TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
return Ui::Text::IconEmoji( return Ui::Text::IconEmoji(
&st::boxStarIconEmoji, &st::starIconEmoji,
QString(QChar(0x2B50))); QString(QChar(0x2B50)));
} }
@ -570,7 +570,7 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
}, st, textFg); }, st, textFg);
} }
void SendStarGift( void SendStarsForm(
not_null<Main::Session*> session, not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data, std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) { Fn<void(std::optional<QString>)> done) {

View file

@ -52,7 +52,7 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
const style::color *textFg = nullptr); const style::color *textFg = nullptr);
void SendStarGift( void SendStarsForm(
not_null<Main::Session*> session, not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data, std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done); Fn<void(std::optional<QString>)> done);

View file

@ -128,6 +128,7 @@ struct GiftDetails {
uint64 randomId = 0; uint64 randomId = 0;
bool anonymous = false; bool anonymous = false;
bool upgraded = false; bool upgraded = false;
bool byStars = false;
}; };
class PreviewDelegate final : public DefaultElementDelegate { class PreviewDelegate final : public DefaultElementDelegate {
@ -497,7 +498,14 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
std::move(details) | rpl::start_with_next([=](GiftDetails details) { std::move(details) | rpl::start_with_next([=](GiftDetails details) {
const auto &descriptor = details.descriptor; const auto &descriptor = details.descriptor;
const auto cost = v::match(descriptor, [&](GiftTypePremium data) { const auto cost = v::match(descriptor, [&](GiftTypePremium data) {
return FillAmountAndCurrency(data.cost, data.currency, true); const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
return stars
? tr::lng_gift_stars_title(tr::now, lt_count, stars)
: FillAmountAndCurrency(data.cost, data.currency, true);
}, [&](GiftTypeStars data) { }, [&](GiftTypeStars data) {
const auto stars = data.info.stars const auto stars = data.info.stars
+ (details.upgraded ? data.info.starsToUpgrade : 0); + (details.upgraded ? data.info.starsToUpgrade : 0);
@ -1118,16 +1126,35 @@ void SendGift(
std::shared_ptr<Api::PremiumGiftCodeOptions> api, std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details, const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) { Fn<void(Payments::CheckoutResult)> done) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
v::match(details.descriptor, [&](const GiftTypePremium &gift) { v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months); if (details.byStars && gift.stars) {
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ auto invoice = Payments::InvoicePremiumGiftCode{
.users = { peer->asUser() }, .purpose = Payments::InvoicePremiumGiftCodeUsers{
.message = details.text, .users = { peer->asUser() },
}; .message = details.text,
Payments::CheckoutProcess::Start(std::move(invoice), done); },
.currency = Ui::kCreditsCurrency,
.randomId = details.randomId,
.amount = uint64(gift.stars),
.storeQuantity = 1,
.users = 1,
.months = gift.months,
};
Payments::CheckoutProcess::Start(
std::move(invoice),
done,
processNonPanelPaymentFormFactory);
} else {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
}, [&](const GiftTypeStars &gift) { }, [&](const GiftTypeStars &gift) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{ Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.info.id, .giftId = gift.info.id,
.randomId = details.randomId, .randomId = details.randomId,
@ -1459,9 +1486,14 @@ void SendGiftBox(
auto cost = state->details.value( auto cost = state->details.value(
) | rpl::map([session](const GiftDetails &details) { ) | rpl::map([session](const GiftDetails &details) {
return v::match(details.descriptor, [&](const GiftTypePremium &data) { return v::match(details.descriptor, [&](const GiftTypePremium &data) {
if (data.currency == kCreditsCurrency) { const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
if (stars) {
return CreditsEmojiSmall(session).append( return CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(std::abs(data.cost))); Lang::FormatCountDecimal(std::abs(stars)));
} }
return TextWithEntities{ return TextWithEntities{
FillAmountAndCurrency(data.cost, data.currency), FillAmountAndCurrency(data.cost, data.currency),
@ -1580,10 +1612,56 @@ void SendGiftBox(
}, container->lifetime()); }, container->lifetime());
AddSkip(container); AddSkip(container);
} }
v::match(descriptor, [&](const GiftTypePremium &) { v::match(descriptor, [&](const GiftTypePremium &data) {
AddDividerText(messageInner, tr::lng_gift_send_premium_about( AddDividerText(messageInner, tr::lng_gift_send_premium_about(
lt_user, lt_user,
rpl::single(peer->shortName()))); rpl::single(peer->shortName())));
if (const auto byStars = data.stars) {
const auto star = Ui::Text::IconEmoji(&st::starIconEmojiColored);
AddSkip(container);
container->add(
object_ptr<SettingsButton>(
container,
tr::lng_gift_send_pay_with_stars(
lt_amount,
rpl::single(base::duplicate(star).append(Lang::FormatCountDecimal(byStars))),
Ui::Text::WithEntities),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.byStars = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
const auto balance = AddDividerText(
container,
tr::lng_gift_send_stars_balance(
lt_amount,
peer->session().credits().balanceValue(
) | rpl::map([=](StarsAmount amount) {
return base::duplicate(star).append(
Lang::FormatStarsAmountDecimal(amount));
}),
lt_link,
tr::lng_gift_send_stars_balance_link(
) | Ui::Text::ToLink(),
Ui::Text::WithEntities));
struct State {
Settings::BuyStarsHandler buyStars;
rpl::variable<bool> loading;
};
const auto state = balance->lifetime().make_state<State>();
state->loading = state->buyStars.loadingValue();
balance->setClickHandlerFilter([=](const auto &...) {
if (!state->loading.current()) {
state->buyStars.handler(window->uiShow())();
}
return false;
});
}
}, [&](const GiftTypeStars &) { }, [&](const GiftTypeStars &) {
AddDividerText(container, peer->isSelf() AddDividerText(container, peer->isSelf()
? tr::lng_gift_send_anonymous_self() ? tr::lng_gift_send_anonymous_self()
@ -1618,9 +1696,13 @@ void SendGiftBox(
const auto weak = MakeWeak(box); const auto weak = MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) { const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) { if (result == Payments::CheckoutResult::Paid) {
if (details.byStars
|| v::is<GiftTypeStars>(details.descriptor)) {
window->session().credits().load(true);
}
const auto copy = state->media; const auto copy = state->media;
window->showPeerHistory(peer); window->showPeerHistory(peer);
ShowSentToast(window, descriptor, details); ShowSentToast(window, details.descriptor, details);
} }
if (const auto strong = weak.data()) { if (const auto strong = weak.data()) {
box->closeBox(); box->closeBox();
@ -1853,6 +1935,8 @@ void GiftBox(
box->setCustomCornersFilling(RectPart::FullTop); box->setCustomCornersFilling(RectPart::FullTop);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
window->session().credits().load();
FillBg(box); FillBg(box);
const auto &stUser = st::premiumGiftsUserpicButton; const auto &stUser = st::premiumGiftsUserpicButton;

View file

@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h" #include "base/qt/qt_common_adapters.h"
@ -1282,7 +1283,7 @@ void SelectTextInFieldWithMargins(
} }
TextWithEntities PaidSendButtonText(tr::now_t, int stars) { TextWithEntities PaidSendButtonText(tr::now_t, int stars) {
return Ui::Text::IconEmoji(&st::boxStarIconEmoji).append( return Ui::Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountToShort(stars).string); Lang::FormatCountToShort(stars).string);
} }

View file

@ -76,6 +76,14 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"first_chat"_q , Command::ChatFirst }, { u"first_chat"_q , Command::ChatFirst },
{ u"last_chat"_q , Command::ChatLast }, { u"last_chat"_q , Command::ChatLast },
{ u"self_chat"_q , Command::ChatSelf }, { u"self_chat"_q , Command::ChatSelf },
{ u"pinned_chat1"_q , Command::ChatPinned1 },
{ u"pinned_chat2"_q , Command::ChatPinned2 },
{ u"pinned_chat3"_q , Command::ChatPinned3 },
{ u"pinned_chat4"_q , Command::ChatPinned4 },
{ u"pinned_chat5"_q , Command::ChatPinned5 },
{ u"pinned_chat6"_q , Command::ChatPinned6 },
{ u"pinned_chat7"_q , Command::ChatPinned7 },
{ u"pinned_chat8"_q , Command::ChatPinned8 },
{ u"previous_folder"_q , Command::FolderPrevious }, { u"previous_folder"_q , Command::FolderPrevious },
{ u"next_folder"_q , Command::FolderNext }, { u"next_folder"_q , Command::FolderNext },

View file

@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h" #include "apiwrap.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_credits.h" // giftBoxByStarsStyle
namespace Data { namespace Data {
namespace { namespace {
@ -1027,6 +1028,14 @@ TextWithEntities CustomEmojiManager::creditsEmoji(QMargins padding) {
false)); false));
} }
TextWithEntities CustomEmojiManager::ministarEmoji(QMargins padding) {
return Ui::Text::SingleCustomEmoji(
registerInternalEmoji(
Ui::GenerateStars(st::giftBoxByStarsStyle.font->height, 1),
padding,
false));
}
QString CustomEmojiManager::registerInternalEmoji( QString CustomEmojiManager::registerInternalEmoji(
QImage emoji, QImage emoji,
QMargins padding, QMargins padding,

View file

@ -100,6 +100,7 @@ public:
[[nodiscard]] uint64 coloredSetId() const; [[nodiscard]] uint64 coloredSetId() const;
[[nodiscard]] TextWithEntities creditsEmoji(QMargins padding = {}); [[nodiscard]] TextWithEntities creditsEmoji(QMargins padding = {});
[[nodiscard]] TextWithEntities ministarEmoji(QMargins padding = {});
private: private:
static constexpr auto kSizeCount = int(SizeTag::kCount); static constexpr auto kSizeCount = int(SizeTag::kCount);

View file

@ -85,13 +85,11 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
unsubscribe(); unsubscribe();
v::match(descriptor, [&](const GiftTypePremium &data) { v::match(descriptor, [&](const GiftTypePremium &data) {
const auto months = data.months; const auto months = data.months;
const auto years = (months % 12) ? 0 : months / 12;
_text = Ui::Text::String(st::giftBoxGiftHeight / 4); _text = Ui::Text::String(st::giftBoxGiftHeight / 4);
_text.setMarkedText( _text.setMarkedText(
st::defaultTextStyle, st::defaultTextStyle,
Ui::Text::Bold(years Ui::Text::Bold(
? tr::lng_years(tr::now, lt_count, years) tr::lng_months(tr::now, lt_count, months)
: tr::lng_months(tr::now, lt_count, months)
).append('\n').append( ).append('\n').append(
tr::lng_gift_premium_label(tr::now) tr::lng_gift_premium_label(tr::now)
)); ));
@ -101,6 +99,18 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
data.cost, data.cost,
data.currency, data.currency,
true)); true));
if (const auto stars = data.stars) {
const auto starsText = QString::number(stars);
_byStars.setMarkedText(
st::giftBoxByStarsStyle,
tr::lng_gift_premium_by_stars(
tr::now,
lt_amount,
_delegate->ministar().append(' ' + starsText),
Ui::Text::WithEntities),
kMarkupTextOptions,
_delegate->textContext());
}
_userpic = nullptr; _userpic = nullptr;
if (!_stars) { if (!_stars) {
_stars.emplace(this, true, starsType); _stars.emplace(this, true, starsType);
@ -170,7 +180,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
QSize(buttonw, buttonh) QSize(buttonw, buttonh)
).marginsAdded(st::giftBoxButtonPadding); ).marginsAdded(st::giftBoxButtonPadding);
const auto skipy = _delegate->buttonSize().height() const auto skipy = _delegate->buttonSize().height()
- st::giftBoxButtonBottom - (_byStars.isEmpty()
? st::giftBoxButtonBottom
: st::giftBoxButtonBottomByStars)
- inner.height(); - inner.height();
const auto skipx = (width() - inner.width()) / 2; const auto skipx = (width() - inner.width()) / 2;
const auto outer = (width() - 2 * skipx); const auto outer = (width() - 2 * skipx);
@ -355,7 +367,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
? st::giftBoxSmallStickerTop ? st::giftBoxSmallStickerTop
: _text.isEmpty() : _text.isEmpty()
? st::giftBoxStickerStarTop ? st::giftBoxStickerStarTop
: st::giftBoxStickerTop), : _byStars.isEmpty()
? st::giftBoxStickerTop
: st::giftBoxStickerTopByStars),
size.width(), size.width(),
size.height()), size.height()),
frame); frame);
@ -367,7 +381,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
? st::giftBoxSmallStickerTop ? st::giftBoxSmallStickerTop
: _text.isEmpty() : _text.isEmpty()
? st::giftBoxStickerStarTop ? st::giftBoxStickerStarTop
: st::giftBoxStickerTop)); : _byStars.isEmpty()
? st::giftBoxStickerTop
: st::giftBoxStickerTopByStars));
_delegate->hiddenMark()->paint( _delegate->hiddenMark()->paint(
p, p,
frame, frame,
@ -473,8 +489,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
if (!_text.isEmpty()) { if (!_text.isEmpty()) {
p.setPen(st::windowFg); p.setPen(st::windowFg);
_text.draw(p, { _text.draw(p, {
.position = (position .position = (position + QPoint(0, _byStars.isEmpty()
+ QPoint(0, st::giftBoxPremiumTextTop)), ? st::giftBoxPremiumTextTop
: st::giftBoxPremiumTextTopByStars)),
.availableWidth = singlew, .availableWidth = singlew,
.align = style::al_top, .align = style::al_top,
}); });
@ -492,6 +509,17 @@ void GiftButton::paintEvent(QPaintEvent *e) {
+ QPoint(padding.left(), padding.top())), + QPoint(padding.left(), padding.top())),
.availableWidth = _price.maxWidth(), .availableWidth = _price.maxWidth(),
}); });
if (!_byStars.isEmpty()) {
p.setPen(st::creditsFg);
_byStars.draw(p, {
.position = QPoint(
position.x(),
_button.y() + _button.height() + st::giftBoxByStarsSkip),
.availableWidth = singlew,
.align = style::al_top,
});
}
} }
} }
@ -515,6 +543,12 @@ TextWithEntities Delegate::star() {
return owner->customEmojiManager().creditsEmoji(); return owner->customEmojiManager().creditsEmoji();
} }
TextWithEntities Delegate::ministar() {
const auto owner = &_window->session().data();
const auto top = st::giftBoxByStarsStarTop;
return owner->customEmojiManager().ministarEmoji({ 0, top, 0, 0 });
}
std::any Delegate::textContext() { std::any Delegate::textContext() {
return Core::MarkedTextContext{ return Core::MarkedTextContext{
.session = &_window->session(), .session = &_window->session(),

View file

@ -102,6 +102,7 @@ enum class GiftButtonMode {
class GiftButtonDelegate { class GiftButtonDelegate {
public: public:
[[nodiscard]] virtual TextWithEntities star() = 0; [[nodiscard]] virtual TextWithEntities star() = 0;
[[nodiscard]] virtual TextWithEntities ministar() = 0;
[[nodiscard]] virtual std::any textContext() = 0; [[nodiscard]] virtual std::any textContext() = 0;
[[nodiscard]] virtual QSize buttonSize() = 0; [[nodiscard]] virtual QSize buttonSize() = 0;
[[nodiscard]] virtual QMargins buttonExtend() = 0; [[nodiscard]] virtual QMargins buttonExtend() = 0;
@ -144,6 +145,7 @@ private:
GiftDescriptor _descriptor; GiftDescriptor _descriptor;
Ui::Text::String _text; Ui::Text::String _text;
Ui::Text::String _price; Ui::Text::String _price;
Ui::Text::String _byStars;
std::shared_ptr<Ui::DynamicImage> _userpic; std::shared_ptr<Ui::DynamicImage> _userpic;
QImage _uniqueBackgroundCache; QImage _uniqueBackgroundCache;
std::unique_ptr<Ui::Text::CustomEmoji> _uniquePatternEmoji; std::unique_ptr<Ui::Text::CustomEmoji> _uniquePatternEmoji;
@ -170,6 +172,7 @@ public:
~Delegate(); ~Delegate();
TextWithEntities star() override; TextWithEntities star() override;
TextWithEntities ministar() override;
std::any textContext() override; std::any textContext() override;
QSize buttonSize() override; QSize buttonSize() override;
QMargins buttonExtend() override; QMargins buttonExtend() override;

View file

@ -66,7 +66,6 @@ void CheckoutProcess::Start(
Mode mode, Mode mode,
Fn<void(CheckoutResult)> reactivate, Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) { Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess;
auto &processes = LookupSessionProcesses(&item->history()->session()); auto &processes = LookupSessionProcesses(&item->history()->session());
const auto media = item->media(); const auto media = item->media();
const auto invoice = media ? media->invoice() : nullptr; const auto invoice = media ? media->invoice() : nullptr;
@ -87,9 +86,7 @@ void CheckoutProcess::Start(
i->second->setReactivateCallback(std::move(reactivate)); i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess( i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess)); std::move(nonPanelPaymentFormProcess));
if (!hasNonPanelPaymentFormProcess) { i->second->requestActivate();
i->second->requestActivate();
}
return; return;
} }
const auto j = processes.byItem.emplace( const auto j = processes.byItem.emplace(
@ -100,9 +97,7 @@ void CheckoutProcess::Start(
std::move(reactivate), std::move(reactivate),
std::move(nonPanelPaymentFormProcess), std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first; PrivateTag{})).first;
if (!hasNonPanelPaymentFormProcess) { j->second->requestActivate();
j->second->requestActivate();
}
} }
void CheckoutProcess::Start( void CheckoutProcess::Start(
@ -110,16 +105,13 @@ void CheckoutProcess::Start(
const QString &slug, const QString &slug,
Fn<void(CheckoutResult)> reactivate, Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) { Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess;
auto &processes = LookupSessionProcesses(session); auto &processes = LookupSessionProcesses(session);
const auto i = processes.bySlug.find(slug); const auto i = processes.bySlug.find(slug);
if (i != end(processes.bySlug)) { if (i != end(processes.bySlug)) {
i->second->setReactivateCallback(std::move(reactivate)); i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess( i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess)); std::move(nonPanelPaymentFormProcess));
if (!hasNonPanelPaymentFormProcess) { i->second->requestActivate();
i->second->requestActivate();
}
return; return;
} }
const auto j = processes.bySlug.emplace( const auto j = processes.bySlug.emplace(
@ -130,20 +122,21 @@ void CheckoutProcess::Start(
std::move(reactivate), std::move(reactivate),
std::move(nonPanelPaymentFormProcess), std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first; PrivateTag{})).first;
if (!hasNonPanelPaymentFormProcess) { j->second->requestActivate();
j->second->requestActivate();
}
} }
void CheckoutProcess::Start( void CheckoutProcess::Start(
InvoicePremiumGiftCode giftCodeInvoice, InvoicePremiumGiftCode giftCodeInvoice,
Fn<void(CheckoutResult)> reactivate) { Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto randomId = giftCodeInvoice.randomId; const auto randomId = giftCodeInvoice.randomId;
auto id = InvoiceId{ std::move(giftCodeInvoice) }; auto id = InvoiceId{ std::move(giftCodeInvoice) };
auto &processes = LookupSessionProcesses(SessionFromId(id)); auto &processes = LookupSessionProcesses(SessionFromId(id));
const auto i = processes.byRandomId.find(randomId); const auto i = processes.byRandomId.find(randomId);
if (i != end(processes.byRandomId)) { if (i != end(processes.byRandomId)) {
i->second->setReactivateCallback(std::move(reactivate)); i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
i->second->requestActivate(); i->second->requestActivate();
return; return;
} }
@ -153,7 +146,7 @@ void CheckoutProcess::Start(
std::move(id), std::move(id),
Mode::Payment, Mode::Payment,
std::move(reactivate), std::move(reactivate),
nullptr, std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first; PrivateTag{})).first;
j->second->requestActivate(); j->second->requestActivate();
} }
@ -372,7 +365,9 @@ void CheckoutProcess::setNonPanelPaymentFormProcess(
} }
void CheckoutProcess::requestActivate() { void CheckoutProcess::requestActivate() {
_panel->requestActivate(); if (!_nonPanelPaymentFormProcess) {
_panel->requestActivate();
}
} }
not_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() { not_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() {

View file

@ -88,7 +88,8 @@ public:
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess); Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
static void Start( static void Start(
InvoicePremiumGiftCode giftCodeInvoice, InvoicePremiumGiftCode giftCodeInvoice,
Fn<void(CheckoutResult)> reactivate); Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess = nullptr);
static void Start( static void Start(
InvoiceCredits creditsInvoice, InvoiceCredits creditsInvoice,
Fn<void(CheckoutResult)> reactivate); Fn<void(CheckoutResult)> reactivate);

View file

@ -219,6 +219,13 @@ MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(
MTP_int(invoice.users)); MTP_int(invoice.users));
} }
bool IsPremiumForStarsInvoice(const InvoiceId &id) {
const auto giftCode = std::get_if<InvoicePremiumGiftCode>(&id.value);
return giftCode
&& !giftCode->creditsAmount
&& (giftCode->currency == ::Ui::kCreditsCurrency);
}
Form::Form(InvoiceId id, bool receipt) Form::Form(InvoiceId id, bool receipt)
: _id(id) : _id(id)
, _session(SessionFromId(id)) , _session(SessionFromId(id))
@ -412,12 +419,29 @@ MTPInputInvoice Form::inputInvoice() const {
MTP_long(giftCode.amount)); MTP_long(giftCode.amount));
const auto users = std::get_if<InvoicePremiumGiftCodeUsers>( const auto users = std::get_if<InvoicePremiumGiftCodeUsers>(
&giftCode.purpose); &giftCode.purpose);
if (users) { auto message = (users && !users->message.empty())
? MTP_textWithEntities(
MTP_string(users->message.text),
Api::EntitiesToMTP(
&users->users.front()->session(),
users->message.entities,
Api::ConvertOption::SkipLocal))
: std::optional<MTPTextWithEntities>();
if (users
&& users->users.size() == 1
&& giftCode.currency == ::Ui::kCreditsCurrency) {
using Flag = MTPDinputInvoicePremiumGiftStars::Flag;
return MTP_inputInvoicePremiumGiftStars(
MTP_flags(message ? Flag::f_message : Flag()),
users->users.front()->inputUser,
MTP_int(giftCode.months),
message.value_or(MTPTextWithEntities()));
} else if (users) {
using Flag = MTPDinputStorePaymentPremiumGiftCode::Flag; using Flag = MTPDinputStorePaymentPremiumGiftCode::Flag;
return MTP_inputInvoicePremiumGiftCode( return MTP_inputInvoicePremiumGiftCode(
MTP_inputStorePaymentPremiumGiftCode( MTP_inputStorePaymentPremiumGiftCode(
MTP_flags((users->boostPeer ? Flag::f_boost_peer : Flag()) MTP_flags((users->boostPeer ? Flag::f_boost_peer : Flag())
| (users->message.empty() ? Flag(0) : Flag::f_message)), | (message ? Flag::f_message : Flag())),
MTP_vector_from_range(ranges::views::all( MTP_vector_from_range(ranges::views::all(
users->users users->users
) | ranges::views::transform([](not_null<UserData*> user) { ) | ranges::views::transform([](not_null<UserData*> user) {
@ -426,12 +450,7 @@ MTPInputInvoice Form::inputInvoice() const {
users->boostPeer ? users->boostPeer->input : MTPInputPeer(), users->boostPeer ? users->boostPeer->input : MTPInputPeer(),
MTP_string(giftCode.currency), MTP_string(giftCode.currency),
MTP_long(giftCode.amount), MTP_long(giftCode.amount),
MTP_textWithEntities( message.value_or(MTPTextWithEntities())),
MTP_string(users->message.text),
Api::EntitiesToMTP(
&users->users.front()->session(),
users->message.entities,
Api::ConvertOption::SkipLocal))),
option); option);
} else { } else {
return MTP_inputInvoicePremiumGiftCode( return MTP_inputInvoicePremiumGiftCode(

View file

@ -287,6 +287,8 @@ struct FormUpdate : std::variant<
[[nodiscard]] MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL( [[nodiscard]] MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(
const InvoicePremiumGiftCode &invoice); const InvoicePremiumGiftCode &invoice);
[[nodiscard]] bool IsPremiumForStarsInvoice(const InvoiceId &id);
class Form final : public base::has_weak_ptr { class Form final : public base::has_weak_ptr {
public: public:
Form(InvoiceId id, bool receipt); Form(InvoiceId id, bool receipt);

View file

@ -57,7 +57,8 @@ void ProcessCreditsPayment(
onstack(CheckoutResult::Cancelled); onstack(CheckoutResult::Cancelled);
} }
return; return;
} else if (form->starGiftForm) { } else if (form->starGiftForm
|| IsPremiumForStarsInvoice(form->id)) {
const auto done = [=](std::optional<QString> error) { const auto done = [=](std::optional<QString> error) {
const auto onstack = maybeReturnToBot; const auto onstack = maybeReturnToBot;
if (error) { if (error) {
@ -86,7 +87,7 @@ void ProcessCreditsPayment(
onstack(CheckoutResult::Paid); onstack(CheckoutResult::Paid);
} }
}; };
Ui::SendStarGift(&show->session(), form, done); Ui::SendStarsForm(&show->session(), form, done);
return; return;
} }
const auto unsuccessful = std::make_shared<bool>(true); const auto unsuccessful = std::make_shared<bool>(true);

View file

@ -429,8 +429,7 @@ void Credits::setupContent() {
Ui::AddSkip(content); Ui::AddSkip(content);
struct State final { struct State final {
rpl::variable<bool> confirmButtonBusy = false; BuyStarsHandler buyStars;
std::optional<Api::CreditsTopupOptions> api;
}; };
const auto state = content->lifetime().make_state<State>(); const auto state = content->lifetime().make_state<State>();
@ -438,60 +437,21 @@ void Credits::setupContent() {
object_ptr<Ui::RoundButton>( object_ptr<Ui::RoundButton>(
content, content,
rpl::conditional( rpl::conditional(
state->confirmButtonBusy.value(), state->buyStars.loadingValue(),
rpl::single(QString()), rpl::single(QString()),
tr::lng_credits_buy_button()), tr::lng_credits_buy_button()),
st::creditsSettingsBigBalanceButton), st::creditsSettingsBigBalanceButton),
st::boxRowPadding); st::boxRowPadding);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
const auto show = _controller->uiShow(); const auto show = _controller->uiShow();
const auto optionsBox = [=](not_null<Ui::GenericBox*> box) { button->setClickedCallback(state->buyStars.handler(show, paid));
box->setStyle(st::giveawayGiftCodeBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_credits_summary_options_subtitle());
const auto inner = box->verticalLayout();
const auto self = show->session().user();
const auto options = state->api
? state->api->options()
: Data::CreditTopupOptions();
const auto amount = StarsAmount();
FillCreditOptions(show, inner, self, amount, paid, nullptr, options);
const auto button = box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
const auto buttonWidth = st::boxWideWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
};
button->setClickedCallback([=] {
if (state->api && !state->api->options().empty()) {
state->confirmButtonBusy = false;
show->show(Box(optionsBox));
} else {
state->confirmButtonBusy = true;
state->api.emplace(show->session().user());
state->api->request(
) | rpl::start_with_error_done([=](const QString &error) {
state->confirmButtonBusy = false;
show->showToast(error);
}, [=] {
state->confirmButtonBusy = false;
show->show(Box(optionsBox));
}, content->lifetime());
}
});
{ {
using namespace Info::Statistics; using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget( const auto loadingAnimation = InfiniteRadialAnimationWidget(
button, button,
button->height() / 2); button->height() / 2);
AddChildToWidgetCenter(button, loadingAnimation); AddChildToWidgetCenter(button, loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value()); loadingAnimation->showOn(state->buyStars.loadingValue());
} }
const auto paddings = rect::m::sum::h(st::boxRowPadding); const auto paddings = rect::m::sum::h(st::boxRowPadding);
button->widthValue() | rpl::filter([=] { button->widthValue() | rpl::filter([=] {
@ -699,4 +659,62 @@ Type CreditsId() {
return Credits::Id(); return Credits::Id();
} }
Fn<void()> BuyStarsHandler::handler(
std::shared_ptr<::Main::SessionShow> show,
Fn<void()> paid) {
const auto optionsBox = [=](not_null<Ui::GenericBox*> box) {
box->setStyle(st::giveawayGiftCodeBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_credits_summary_options_subtitle());
const auto inner = box->verticalLayout();
const auto self = show->session().user();
const auto options = _api
? _api->options()
: Data::CreditTopupOptions();
const auto amount = StarsAmount();
const auto weak = Ui::MakeWeak(box);
FillCreditOptions(show, inner, self, amount, [=] {
if (const auto strong = weak.data()) {
strong->closeBox();
}
if (const auto onstack = paid) {
onstack();
}
}, nullptr, options);
const auto button = box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
const auto buttonWidth = st::boxWideWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
};
return crl::guard(this, [=] {
if (_api && !_api->options().empty()) {
_loading = false;
show->show(Box(crl::guard(this, optionsBox)));
} else {
_loading = true;
const auto user = show->session().user();
_api = std::make_unique<Api::CreditsTopupOptions>(user);
_api->request(
) | rpl::start_with_error_done([=](const QString &error) {
_loading = false;
show->showToast(error);
}, [=] {
_loading = false;
show->show(Box(crl::guard(this, optionsBox)));
}, _lifetime);
}
});
}
rpl::producer<bool> BuyStarsHandler::loadingValue() const {
return _loading.value();
}
} // namespace Settings } // namespace Settings

View file

@ -9,9 +9,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_type.h" #include "settings/settings_type.h"
namespace Api {
class CreditsTopupOptions;
} // namespace Api
namespace Main {
class SessionShow;
} // namespace Main
namespace Settings { namespace Settings {
[[nodiscard]] Type CreditsId(); [[nodiscard]] Type CreditsId();
} // namespace Settings class BuyStarsHandler final : public base::has_weak_ptr {
public:
[[nodiscard]] Fn<void()> handler(
std::shared_ptr<::Main::SessionShow> show,
Fn<void()> paid = nullptr);
[[nodiscard]] rpl::producer<bool> loadingValue() const;
private:
std::unique_ptr<Api::CreditsTopupOptions> _api;
rpl::variable<bool> _loading;
rpl::lifetime _lifetime;
};
} // namespace Settings

View file

@ -569,8 +569,8 @@ void FillCreditOptions(
if (const auto strong = weak.data()) { if (const auto strong = weak.data()) {
strong->window()->setFocus(); strong->window()->setFocus();
if (result == Payments::CheckoutResult::Paid) { if (result == Payments::CheckoutResult::Paid) {
if (paid) { if (const auto onstack = paid) {
paid(); onstack();
} }
} }
} }

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
namespace Ui { namespace Ui {
namespace { namespace {
@ -54,7 +55,7 @@ void SendButton::setState(State state) {
|| _state.starsToSend != state.starsToSend) { || _state.starsToSend != state.starsToSend) {
_starsToSendText.setMarkedText( _starsToSendText.setMarkedText(
_st.stars.style, _st.stars.style,
Text::IconEmoji(&st::boxStarIconEmoji).append( Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountToShort(state.starsToSend).string), Lang::FormatCountToShort(state.starsToSend).string),
kMarkupTextOptions); kMarkupTextOptions);
} }

View file

@ -63,11 +63,12 @@ creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) {
} }
starIconEmoji: IconEmoji { starIconEmoji: IconEmoji {
icon: icon{{ "payments/small_star", windowFg }}; icon: icon{{ "payments/premium_emoji", creditsBg1 }};
padding: margins(0px, -3px, 0px, 0px); padding: margins(4px, 1px, 4px, 0px);
}
starIconEmojiColored: IconEmoji(starIconEmoji) {
useIconColor: true;
} }
starIconSmall: icon{{ "payments/small_star", windowFg }};
starIconSmallPadding: margins(0px, -3px, 0px, 0px);
creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }}; creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }};
@ -123,10 +124,17 @@ giftBoxGiftHeight: 164px;
giftBoxGiftSmall: 108px; giftBoxGiftSmall: 108px;
giftBoxGiftRadius: 12px; giftBoxGiftRadius: 12px;
giftBoxGiftBadgeFont: font(10px semibold); giftBoxGiftBadgeFont: font(10px semibold);
giftBoxByStarsStyle: TextStyle(defaultTextStyle) {
font: font(10px);
}
giftBoxByStarsSkip: 2px;
giftBoxByStarsStarTop: 3px;
giftBoxPremiumIconSize: 64px; giftBoxPremiumIconSize: 64px;
giftBoxPremiumIconTop: 10px; giftBoxPremiumIconTop: 10px;
giftBoxPremiumTextTop: 84px; giftBoxPremiumTextTop: 84px;
giftBoxPremiumTextTopByStars: 78px;
giftBoxButtonBottom: 12px; giftBoxButtonBottom: 12px;
giftBoxButtonBottomByStars: 18px;
giftBoxButtonPadding: margins(8px, 4px, 8px, 4px); giftBoxButtonPadding: margins(8px, 4px, 8px, 4px);
giftBoxPreviewStickerPadding: margins(10px, 12px, 10px, 16px); giftBoxPreviewStickerPadding: margins(10px, 12px, 10px, 16px);
giftBoxPreviewTitlePadding: margins(12px, 4px, 12px, 4px); giftBoxPreviewTitlePadding: margins(12px, 4px, 12px, 4px);
@ -135,6 +143,7 @@ giftBoxButtonMargin: margins(12px, 8px, 12px, 12px);
giftBoxStickerTop: 0px; giftBoxStickerTop: 0px;
giftBoxStickerStarTop: 24px; giftBoxStickerStarTop: 24px;
giftBoxSmallStickerTop: 16px; giftBoxSmallStickerTop: 16px;
giftBoxStickerTopByStars: -4px;
giftBoxStickerSize: size(80px, 80px); giftBoxStickerSize: size(80px, 80px);
giftBoxUserpicSize: 24px; giftBoxUserpicSize: 24px;
giftBoxUserpicSkip: 2px; giftBoxUserpicSkip: 2px;

@ -1 +1 @@
Subproject commit da0d634ec373d8ba0b8f66f3381a87e43edffdf2 Subproject commit 76f25481470faee2e0a24aa7be8c6d58564addea