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_features" = "See Features >";
"lng_gift_premium_label" = "Premium";
"lng_gift_premium_by_stars" = "or {amount}";
"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_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_message" = "Enter Message";
"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_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.";

View file

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

View file

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

View file

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

View file

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

View file

@ -128,6 +128,7 @@ struct GiftDetails {
uint64 randomId = 0;
bool anonymous = false;
bool upgraded = false;
bool byStars = false;
};
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) {
const auto &descriptor = details.descriptor;
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) {
const auto stars = data.info.stars
+ (details.upgraded ? data.info.starsToUpgrade : 0);
@ -1118,16 +1126,35 @@ void SendGift(
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
if (details.byStars && gift.stars) {
auto invoice = Payments::InvoicePremiumGiftCode{
.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
},
.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 auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.info.id,
.randomId = details.randomId,
@ -1459,9 +1486,14 @@ void SendGiftBox(
auto cost = state->details.value(
) | rpl::map([session](const GiftDetails &details) {
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(
Lang::FormatCountDecimal(std::abs(data.cost)));
Lang::FormatCountDecimal(std::abs(stars)));
}
return TextWithEntities{
FillAmountAndCurrency(data.cost, data.currency),
@ -1580,10 +1612,56 @@ void SendGiftBox(
}, container->lifetime());
AddSkip(container);
}
v::match(descriptor, [&](const GiftTypePremium &) {
v::match(descriptor, [&](const GiftTypePremium &data) {
AddDividerText(messageInner, tr::lng_gift_send_premium_about(
lt_user,
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 &) {
AddDividerText(container, peer->isSelf()
? tr::lng_gift_send_anonymous_self()
@ -1618,9 +1696,13 @@ void SendGiftBox(
const auto weak = MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) {
if (details.byStars
|| v::is<GiftTypeStars>(details.descriptor)) {
window->session().credits().load(true);
}
const auto copy = state->media;
window->showPeerHistory(peer);
ShowSentToast(window, descriptor, details);
ShowSentToast(window, details.descriptor, details);
}
if (const auto strong = weak.data()) {
box->closeBox();
@ -1853,6 +1935,8 @@ void GiftBox(
box->setCustomCornersFilling(RectPart::FullTop);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
window->session().credits().load();
FillBg(box);
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_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
@ -1282,7 +1283,7 @@ void SelectTextInFieldWithMargins(
}
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);
}

View file

@ -76,6 +76,14 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"first_chat"_q , Command::ChatFirst },
{ u"last_chat"_q , Command::ChatLast },
{ 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"next_folder"_q , Command::FolderNext },

View file

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

View file

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

View file

@ -85,13 +85,11 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
unsubscribe();
v::match(descriptor, [&](const GiftTypePremium &data) {
const auto months = data.months;
const auto years = (months % 12) ? 0 : months / 12;
_text = Ui::Text::String(st::giftBoxGiftHeight / 4);
_text.setMarkedText(
st::defaultTextStyle,
Ui::Text::Bold(years
? tr::lng_years(tr::now, lt_count, years)
: tr::lng_months(tr::now, lt_count, months)
Ui::Text::Bold(
tr::lng_months(tr::now, lt_count, months)
).append('\n').append(
tr::lng_gift_premium_label(tr::now)
));
@ -101,6 +99,18 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
data.cost,
data.currency,
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;
if (!_stars) {
_stars.emplace(this, true, starsType);
@ -170,7 +180,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) {
QSize(buttonw, buttonh)
).marginsAdded(st::giftBoxButtonPadding);
const auto skipy = _delegate->buttonSize().height()
- st::giftBoxButtonBottom
- (_byStars.isEmpty()
? st::giftBoxButtonBottom
: st::giftBoxButtonBottomByStars)
- inner.height();
const auto skipx = (width() - inner.width()) / 2;
const auto outer = (width() - 2 * skipx);
@ -355,7 +367,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
? st::giftBoxSmallStickerTop
: _text.isEmpty()
? st::giftBoxStickerStarTop
: st::giftBoxStickerTop),
: _byStars.isEmpty()
? st::giftBoxStickerTop
: st::giftBoxStickerTopByStars),
size.width(),
size.height()),
frame);
@ -367,7 +381,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
? st::giftBoxSmallStickerTop
: _text.isEmpty()
? st::giftBoxStickerStarTop
: st::giftBoxStickerTop));
: _byStars.isEmpty()
? st::giftBoxStickerTop
: st::giftBoxStickerTopByStars));
_delegate->hiddenMark()->paint(
p,
frame,
@ -473,8 +489,9 @@ void GiftButton::paintEvent(QPaintEvent *e) {
if (!_text.isEmpty()) {
p.setPen(st::windowFg);
_text.draw(p, {
.position = (position
+ QPoint(0, st::giftBoxPremiumTextTop)),
.position = (position + QPoint(0, _byStars.isEmpty()
? st::giftBoxPremiumTextTop
: st::giftBoxPremiumTextTopByStars)),
.availableWidth = singlew,
.align = style::al_top,
});
@ -492,6 +509,17 @@ void GiftButton::paintEvent(QPaintEvent *e) {
+ QPoint(padding.left(), padding.top())),
.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();
}
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() {
return Core::MarkedTextContext{
.session = &_window->session(),

View file

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

View file

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

View file

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

View file

@ -219,6 +219,13 @@ MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(
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)
: _id(id)
, _session(SessionFromId(id))
@ -412,12 +419,29 @@ MTPInputInvoice Form::inputInvoice() const {
MTP_long(giftCode.amount));
const auto users = std::get_if<InvoicePremiumGiftCodeUsers>(
&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;
return MTP_inputInvoicePremiumGiftCode(
MTP_inputStorePaymentPremiumGiftCode(
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(
users->users
) | ranges::views::transform([](not_null<UserData*> user) {
@ -426,12 +450,7 @@ MTPInputInvoice Form::inputInvoice() const {
users->boostPeer ? users->boostPeer->input : MTPInputPeer(),
MTP_string(giftCode.currency),
MTP_long(giftCode.amount),
MTP_textWithEntities(
MTP_string(users->message.text),
Api::EntitiesToMTP(
&users->users.front()->session(),
users->message.entities,
Api::ConvertOption::SkipLocal))),
message.value_or(MTPTextWithEntities())),
option);
} else {
return MTP_inputInvoicePremiumGiftCode(

View file

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

View file

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

View file

@ -429,8 +429,7 @@ void Credits::setupContent() {
Ui::AddSkip(content);
struct State final {
rpl::variable<bool> confirmButtonBusy = false;
std::optional<Api::CreditsTopupOptions> api;
BuyStarsHandler buyStars;
};
const auto state = content->lifetime().make_state<State>();
@ -438,60 +437,21 @@ void Credits::setupContent() {
object_ptr<Ui::RoundButton>(
content,
rpl::conditional(
state->confirmButtonBusy.value(),
state->buyStars.loadingValue(),
rpl::single(QString()),
tr::lng_credits_buy_button()),
st::creditsSettingsBigBalanceButton),
st::boxRowPadding);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
const auto show = _controller->uiShow();
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 = 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());
}
});
button->setClickedCallback(state->buyStars.handler(show, paid));
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
button,
button->height() / 2);
AddChildToWidgetCenter(button, loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
loadingAnimation->showOn(state->buyStars.loadingValue());
}
const auto paddings = rect::m::sum::h(st::boxRowPadding);
button->widthValue() | rpl::filter([=] {
@ -699,4 +659,62 @@ Type CreditsId() {
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

View file

@ -9,9 +9,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_type.h"
namespace Api {
class CreditsTopupOptions;
} // namespace Api
namespace Main {
class SessionShow;
} // namespace Main
namespace Settings {
[[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()) {
strong->window()->setFocus();
if (result == Payments::CheckoutResult::Paid) {
if (paid) {
paid();
if (const auto onstack = paid) {
onstack();
}
}
}

View file

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

View file

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

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