Allow sending premium / star gifts.

This commit is contained in:
John Preston 2024-09-17 18:59:24 +04:00
parent 71deef61f5
commit 0d0d0ab994
13 changed files with 407 additions and 109 deletions

View file

@ -1853,11 +1853,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_proximity_distance_km#other" = "{count} km";
"lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot.";
"lng_action_gift_received" = "{user} sent you a gift for {cost}";
"lng_action_gift_received_me" = "You sent to {user} a gift for {cost}";
"lng_action_gift_sent" = "You sent a gift for {cost}";
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
"lng_action_gift_anonymous" = "Anonymous Gift";
"lng_action_gift_for_stars#one" = "{count} Star";
"lng_action_gift_for_stars#other" = "{count} Stars";
"lng_action_gift_got_subtitle" = "Gift from {user}";
"lng_action_gift_got_stars_text" = "Display this gift on your page or convert it to {cost}.";
"lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
"lng_action_gift_sent_subtitle" = "Gift for {user}";
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo";

View file

@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/gift_credits_box.h"
#include "base/random.h"
#include "api/api_credits.h"
#include "api/api_premium.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/send_credits_box.h"
#include "chat_helpers/stickers_gift_box_pack.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/click_handler_types.h"
#include "core/local_url_handlers.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -27,6 +30,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "main/session/session_show.h"
#include "main/main_session.h"
#include "payments/payments_form.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_credits_graphics.h"
#include "settings/settings_credits.h"
#include "settings/settings_premium.h"
@ -38,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/format_values.h"
@ -83,6 +90,11 @@ struct GiftTypePremium {
const GiftTypePremium &) = default;
};
struct PremiumGiftsDescriptor {
std::vector<GiftTypePremium> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct GiftTypeStars {
uint64 id = 0;
int64 stars = 0;
@ -102,6 +114,11 @@ struct GiftDescriptor : std::variant<GiftTypePremium, GiftTypeStars> {
const GiftDescriptor&) = default;
};
struct GiftsDescriptor {
std::vector<GiftDescriptor> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct GiftDetails {
GiftDescriptor descriptor;
QString text;
@ -174,6 +191,8 @@ auto GenerateGiftMedia(
Element *replacing,
const GiftDetails &data)
-> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
Expects(v::is<GiftTypeStars>(data.descriptor));
return [=](Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto &descriptor = data.descriptor;
auto pushText = [&](
@ -212,27 +231,15 @@ auto GenerateGiftMedia(
replacing,
sticker,
st::giftBoxPreviewStickerPadding));
const auto title = data.anonymous
? tr::lng_action_gift_anonymous(tr::now)
: tr::lng_action_gift_got_subtitle(
tr::now,
lt_user,
parent->data()->history()->session().user()->shortName());
auto textFallback = v::match(descriptor, [&](GiftTypePremium data) {
return TextWithEntities{
u"Use all those premium features with joy!"_q
};
}, [&](GiftTypeStars data) {
return tr::lng_action_gift_got_stars_text(
tr::now,
lt_cost,
tr::lng_gift_stars_title(
tr::now,
lt_count,
data.stars,
Ui::Text::Bold),
Ui::Text::WithEntities);
});
const auto title = tr::lng_action_gift_got_subtitle(
tr::now,
lt_user,
parent->data()->history()->session().user()->shortName());
auto textFallback = tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
v::get<GiftTypeStars>(descriptor).stars,
Ui::Text::RichLangValue);
auto description = data.text.isEmpty()
? std::move(textFallback)
: TextWithEntities{ data.text };
@ -284,14 +291,12 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
}, [&](GiftTypeStars data) {
return tr::lng_gift_stars_title(tr::now, lt_count, data.stars);
});
const auto text = details.anonymous
? tr::lng_action_gift_received_anonymous(tr::now, lt_cost, cost)
: tr::lng_action_gift_received(
tr::now,
lt_user,
_history->session().user()->shortName(),
lt_cost,
cost);
const auto text = tr::lng_action_gift_received(
tr::now,
lt_user,
_history->session().user()->shortName(),
lt_cost,
cost);
const auto item = _history->makeMessage({
.id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeAboutView
@ -355,11 +360,11 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
_item->draw(p, context);
}
[[nodiscard]] rpl::producer<std::vector<GiftTypePremium>> GiftsPremium(
[[nodiscard]] rpl::producer<PremiumGiftsDescriptor> GiftsPremium(
not_null<Main::Session*> session,
not_null<PeerData*> peer) {
struct Session {
std::vector<GiftTypePremium> last;
PremiumGiftsDescriptor last;
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
return [=](auto consumer) {
@ -370,12 +375,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
i = Map.emplace(session, Session()).first;
session->lifetime().add([=] { Map.remove(session); });
}
if (!i->second.last.empty()) {
if (!i->second.last.list.empty()) {
consumer.put_next_copy(i->second.last);
}
using namespace Api;
const auto api = lifetime.make_state<PremiumGiftCodeOptions>(peer);
const auto api = std::make_shared<PremiumGiftCodeOptions>(peer);
api->request() | rpl::start_with_error_done([=](QString error) {
consumer.put_next({});
}, [=] {
@ -411,9 +416,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
}
ranges::sort(list, ranges::less(), &GiftTypePremium::months);
auto &map = Map[session];
if (map.last != list) {
map.last = list;
consumer.put_next_copy(list);
if (map.last.list != list) {
map.last = PremiumGiftsDescriptor{
std::move(list),
api,
};
consumer.put_next_copy(map.last);
}
}, lifetime);
@ -923,6 +931,9 @@ void SendGiftBox(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
const GiftDescriptor &descriptor) {
Expects(v::is<GiftTypeStars>(descriptor));
const auto gift = v::get<GiftTypeStars>(descriptor);
box->setStyle(st::giftBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_gift_send_title());
@ -949,21 +960,11 @@ void SendGiftBox(
Lang::FormatCountDecimal(std::abs(data.stars)));
});
}());
const auto button = box->addButton(rpl::single(QString()), [=] {
box->closeBox();
});
SetButtonMarkedLabel(
button,
tr::lng_gift_send_button(
lt_cost,
std::move(cost),
Ui::Text::WithEntities),
session,
st::creditsBoxButtonLabel,
st::giftBox.button.textFg->c);
struct State {
rpl::variable<GiftDetails> details;
uint64 randomId = 0;
bool submitting = false;
};
const auto state = box->lifetime().make_state<State>();
state->details = GiftDetails{
@ -1010,6 +1011,33 @@ void SendGiftBox(
const auto buttonWidth = st::boxWideWidth
- st::giftBox.buttonPadding.left()
- st::giftBox.buttonPadding.right();
const auto button = box->addButton(rpl::single(QString()), [=] {
if (state->submitting) {
return;
}
state->submitting = true;
state->randomId = base::RandomValue<uint64>();
const auto details = state->details.current();
const auto done = [=](Payments::CheckoutResult result) {
box->closeBox();
};
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.id,
.randomId = state->randomId,
.message = { details.text },
.user = peer->asUser(),
.anonymous = details.anonymous,
}, done, Payments::ProcessNonPanelPaymentFormFactory(window, done));
});
SetButtonMarkedLabel(
button,
tr::lng_gift_send_button(
lt_cost,
std::move(cost),
Ui::Text::WithEntities),
session,
st::creditsBoxButtonLabel,
st::giftBox.button.textFg->c);
button->resizeToWidth(buttonWidth);
button->widthValue() | rpl::start_with_next([=](int width) {
if (width != buttonWidth) {
@ -1018,10 +1046,23 @@ void SendGiftBox(
}, button->lifetime());
}
void SendPremiumGift(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftTypePremium &gift,
Fn<void(Payments::CheckoutResult)> done) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
{ peer->asUser() }
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<std::vector<GiftDescriptor>> gifts) {
rpl::producer<GiftsDescriptor> gifts) {
auto result = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = result.data();
@ -1062,7 +1103,7 @@ void SendGiftBox(
return _bg;
}
DocumentData *lookupSticker(
const GiftDescriptor &descriptor) {
const GiftDescriptor &descriptor) override {
const auto &session = _window->session();
auto &packs = session.giftBoxStickersPacks();
packs.load();
@ -1086,6 +1127,7 @@ void SendGiftBox(
struct State {
Delegate delegate;
std::vector<std::unique_ptr<GiftButton>> buttons;
bool sending = false;
};
const auto state = raw->lifetime().make_state<State>(State{
.delegate = Delegate(window, peer),
@ -1136,23 +1178,24 @@ void SendGiftBox(
state->delegate.setBackground(std::move(bg));
std::move(
gifts
) | rpl::start_with_next([=](const std::vector<GiftDescriptor> &gifts) {
) | rpl::start_with_next([=](const GiftsDescriptor &gifts) {
const auto width = st::boxWideWidth;
const auto padding = st::giftBoxPadding;
const auto available = width - padding.left() - padding.right();
auto x = padding.left();
auto y = padding.top();
state->buttons.resize(gifts.size());
state->buttons.resize(gifts.list.size());
for (auto &button : state->buttons) {
if (!button) {
button = std::make_unique<GiftButton>(raw, &state->delegate);
button->show();
}
}
for (auto i = 0, count = int(gifts.size()); i != count; ++i) {
const auto api = gifts.api;
for (auto i = 0, count = int(gifts.list.size()); i != count; ++i) {
const auto button = state->buttons[i].get();
const auto &descriptor = gifts[i];
const auto &descriptor = gifts.list[i];
button->setDescriptor(descriptor);
const auto last = !((i + 1) % kGiftsPerRow);
@ -1167,16 +1210,36 @@ void SendGiftBox(
x += single.width() + st::giftBoxGiftSkip.x();
}
const auto premiumSent = [=](Payments::CheckoutResult result) {
state->sending = false;
if (result == Payments::CheckoutResult::Paid) {
window->showToast(u"Sent!"_q);
}
};
button->setClickedCallback([=] {
window->show(Box(SendGiftBox, window, peer, descriptor));
if (v::is<GiftTypePremium>(descriptor)) {
if (state->sending) {
return;
} else {
state->sending = true;
}
SendPremiumGift(
window,
peer,
api,
v::get<GiftTypePremium>(descriptor),
premiumSent);
} else {
window->show(Box(SendGiftBox, window, peer, descriptor));
}
});
}
if (gifts.size() % kGiftsPerRow) {
if (gifts.list.size() % kGiftsPerRow) {
y += padding.bottom() + single.height();
} else {
y += padding.bottom() - st::giftBoxGiftSkip.y();
}
raw->resize(raw->width(), gifts.empty() ? 0 : y);
raw->resize(raw->width(), gifts.list.empty() ? 0 : y);
}, raw->lifetime());
return result;
@ -1228,16 +1291,20 @@ void AddBlock(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
struct State {
rpl::variable<std::vector<GiftDescriptor>> gifts;
rpl::variable<PremiumGiftsDescriptor> gifts;
};
auto state = std::make_unique<State>();
state->gifts = GiftsPremium(&window->session(), peer) | rpl::map([=](
const std::vector<GiftTypePremium> &gifts) {
return gifts | ranges::to<std::vector<GiftDescriptor>>;
});
state->gifts = GiftsPremium(&window->session(), peer);
;
auto result = MakeGiftsList(window, peer, state->gifts.value());
auto result = MakeGiftsList(window, peer, state->gifts.value(
) | rpl::map([=](const PremiumGiftsDescriptor &gifts) {
return GiftsDescriptor{
gifts.list | ranges::to<std::vector<GiftDescriptor>>,
gifts.api,
};
}));
result->lifetime().add([state = std::move(state)] {});
return result;
}
@ -1267,7 +1334,9 @@ void AddBlock(
? (!gift.limited)
: (price && gift.stars != price);
}), end(gifts));
return gifts | ranges::to<std::vector<GiftDescriptor>>();
return GiftsDescriptor{
gifts | ranges::to<std::vector<GiftDescriptor>>(),
};
})));
return result;

View file

@ -440,4 +440,22 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
}, st, textFg);
}
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {
session->api().request(MTPpayments_SendStarsForm(
MTP_long(data->formId),
data->inputInvoice
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
done(std::nullopt);
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
} // namespace Ui

View file

@ -52,4 +52,9 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
const style::FlatLabel &st,
std::optional<QColor> textFg = {});
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);
} // namespace Ui

View file

@ -130,16 +130,24 @@ struct GiveawayResults {
enum class GiftType : uchar {
Premium, // count - months
Credits, // count - credits
StarGift, // count - stars
};
struct GiftCode {
QString slug;
DocumentData *document = nullptr;
TextWithEntities message;
ChannelData *channel = nullptr;
MsgId giveawayMsgId = 0;
int convertStars = 0;
int limitedCount = 0;
int count = 0;
int giveawayMsgId = 0;
GiftType type = GiftType::Premium;
bool viaGiveaway = false;
bool unclaimed = false;
bool viaGiveaway : 1 = false;
bool unclaimed : 1 = false;
bool anonymous : 1 = false;
bool converted : 1 = false;
bool saved : 1 = false;
};
class Media {

View file

@ -4917,12 +4917,15 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
Ui::Text::WithEntities);
} else {
result.links.push_back(peer->createOpenLink());
result.text = (isSelf
? tr::lng_action_gift_received_me
: tr::lng_action_gift_received)(
result.text = isSelf
? tr::lng_action_gift_sent(tr::now,
lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now,
lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1.
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
cost,
Ui::Text::WithEntities);
@ -5130,18 +5133,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
} else {
const auto isSelf = (_from->id == _from->session().userPeerId());
const auto peer = isSelf ? _history->peer : _from;
const auto cost = AmountAndStarCurrency(
&_history->session(),
action.vamount().value_or_empty(),
qs(action.vcurrency().value_or_empty()));
result.links.push_back(peer->createOpenLink());
result.text = (isSelf
? tr::lng_action_gift_received_me
: tr::lng_action_gift_received)(
result.text = isSelf
? tr::lng_action_gift_sent(tr::now,
lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now,
lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1.
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
AmountAndStarCurrency(
&_history->session(),
action.vamount().value_or_empty(),
qs(action.vcurrency().value_or_empty())),
cost,
Ui::Text::WithEntities);
}
@ -5238,18 +5245,22 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
_history->session().giftBoxStickersPacks().load();
const auto amount = action.vamount().v;
const auto currency = qs(action.vcurrency());
const auto cost = AmountAndStarCurrency(
&_history->session(),
amount,
currency);
result.links.push_back(peer->createOpenLink());
result.text = (isSelf
? tr::lng_action_gift_received_me
: tr::lng_action_gift_received)(
result.text = isSelf
? tr::lng_action_gift_sent(tr::now,
lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now,
lt_user,
Ui::Text::Link(peer->name(), 1), // Link 1.
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
AmountAndStarCurrency(
&_history->session(),
amount,
currency),
cost,
Ui::Text::WithEntities);
return result;
};
@ -5273,6 +5284,34 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
auto prepareStarGift = [&](
const MTPDmessageActionStarGift &action) {
auto result = PreparedServiceText();
const auto isSelf = _from->isSelf();
const auto peer = isSelf ? _history->peer : _from;
const auto stars = action.vgift().data().vstars().v;
const auto cost = TextWithEntities{
tr::lng_action_gift_for_stars(tr::now, lt_count, stars),
};
const auto anonymous = _from->isServiceUser();
if (anonymous) {
result.text = tr::lng_action_gift_received_anonymous(
tr::now,
lt_cost,
cost,
Ui::Text::WithEntities);
} else {
result.links.push_back(peer->createOpenLink());
result.text = isSelf
? tr::lng_action_gift_sent(tr::now,
lt_cost,
cost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received(
tr::now,
lt_user,
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
cost,
Ui::Text::WithEntities);
}
return result;
};
@ -5428,18 +5467,35 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
.slug = qs(data.vtransaction_id()),
.channel = history()->owner().channel(
peerToChannel(peerFromMTP(data.vboost_peer()))),
.count = int(data.vstars().v),
.giveawayMsgId = data.vgiveaway_msg_id().v,
.count = int(data.vstars().v),
.type = Data::GiftType::Credits,
.viaGiveaway = true,
.unclaimed = data.is_unclaimed(),
});
}, [&](const MTPDmessageActionStarGift &data) {
_media = std::make_unique<Data::MediaGiftBox>(
this,
_from,
Data::GiftType::Credits,
data.vstars_amount().v);
const auto &gift = data.vgift().data();
const auto document = history()->owner().processDocument(
gift.vsticker());
using Fields = Data::GiftCode;
_media = std::make_unique<Data::MediaGiftBox>(this, _from, Fields{
.document = document->sticker() ? document.get() : nullptr,
.message = (data.vmessage()
? TextWithEntities{
.text = qs(data.vmessage()->data().vtext()),
.entities = Api::EntitiesFromMTP(
&history()->session(),
data.vmessage()->data().ventities().v),
}
: TextWithEntities()),
.convertStars = int(data.vconvert_stars().v),
.limitedCount = gift.vavailability_total().value_or_empty(),
.count = int(gift.vstars().v),
.type = Data::GiftType::StarGift,
.anonymous = data.is_name_hidden(),
.converted = data.is_converted(),
.saved = data.is_saved(),
});
}, [](const auto &) {
});
}

View file

@ -52,7 +52,14 @@ QSize PremiumGift::size() {
}
QString PremiumGift::title() {
if (creditsPrize()) {
if (starGift()) {
return (outgoingGift()
? tr::lng_action_gift_sent_subtitle
: tr::lng_action_gift_got_subtitle)(
tr::now,
lt_user,
_parent->history()->peer->shortName());
} else if (creditsPrize()) {
return tr::lng_prize_title(tr::now);
} else if (const auto count = credits()) {
return tr::lng_gift_stars_title(tr::now, lt_count, count);
@ -65,6 +72,23 @@ QString PremiumGift::title() {
}
TextWithEntities PremiumGift::subtitle() {
if (starGift()) {
return !_data.message.empty()
? _data.message
: outgoingGift()
? tr::lng_action_gift_sent_text(
tr::now,
lt_count,
_data.count,
lt_user,
Ui::Text::Bold(_parent->history()->peer->shortName()),
Ui::Text::RichLangValue)
: tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
_data.count,
Ui::Text::RichLangValue);
}
const auto isCreditsPrize = creditsPrize();
if (const auto count = credits(); count && !isCreditsPrize) {
return outgoingGift()
@ -111,7 +135,9 @@ TextWithEntities PremiumGift::subtitle() {
}
rpl::producer<QString> PremiumGift::button() {
return creditsPrize()
return (starGift() && outgoingGift())
? nullptr
: creditsPrize()
? tr::lng_view_button_giftcode()
: (gift() && (outgoingGift() || !_data.unclaimed))
? tr::lng_sticker_premium_view()
@ -119,6 +145,9 @@ rpl::producer<QString> PremiumGift::button() {
}
ClickHandlerPtr PremiumGift::createViewLink() {
if (starGift() && outgoingGift()) {
return nullptr;
}
const auto from = _gift->from();
const auto peer = _parent->history()->peer;
const auto date = _parent->data()->date();
@ -142,7 +171,7 @@ ClickHandlerPtr PremiumGift::createViewLink() {
.barePeerId = data.channel
? data.channel->id.value
: 0,
.bareGiveawayMsgId = uint64(data.giveawayMsgId),
.bareGiveawayMsgId = uint64(data.giveawayMsgId.bare),
.peerType = Type::Peer,
.in = true,
},
@ -223,6 +252,10 @@ bool PremiumGift::gift() const {
return _data.slug.isEmpty() || !_data.channel;
}
bool PremiumGift::starGift() const {
return _data.type == Data::GiftType::StarGift;
}
bool PremiumGift::creditsPrize() const {
return _data.viaGiveaway
&& (_data.type == Data::GiftType::Credits)
@ -236,6 +269,14 @@ int PremiumGift::credits() const {
void PremiumGift::ensureStickerCreated() const {
if (_sticker) {
return;
} else if (const auto document = _data.document) {
if (const auto sticker = document->sticker()) {
const auto skipPremiumEffect = false;
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
_sticker->setDiceIndex(sticker->alt, 1);
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
return;
}
}
const auto &session = _parent->history()->session();
auto &packs = session.giftBoxStickersPacks();

View file

@ -48,6 +48,7 @@ public:
private:
[[nodiscard]] bool incomingGift() const;
[[nodiscard]] bool outgoingGift() const;
[[nodiscard]] bool starGift() const;
[[nodiscard]] bool gift() const;
[[nodiscard]] bool creditsPrize() const;
[[nodiscard]] int credits() const;

View file

@ -181,6 +181,30 @@ void CheckoutProcess::Start(
j->second->requestActivate();
}
void CheckoutProcess::Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto randomId = giftInvoice.randomId;
auto id = InvoiceId{ std::move(giftInvoice) };
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));
return;
}
processes.byRandomId.emplace(
randomId,
std::make_unique<CheckoutProcess>(
std::move(id),
Mode::Payment,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{}));
}
std::optional<PaidInvoice> CheckoutProcess::InvoicePaid(
not_null<const HistoryItem*> item) {
const auto session = &item->history()->session();

View file

@ -38,6 +38,7 @@ class Form;
struct FormUpdate;
struct Error;
struct InvoiceCredits;
struct InvoiceStarGift;
struct InvoiceId;
struct InvoicePremiumGiftCode;
struct CreditsFormData;
@ -91,6 +92,10 @@ public:
static void Start(
InvoiceCredits creditsInvoice,
Fn<void(CheckoutResult)> reactivate);
static void Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(
not_null<const HistoryItem*> item);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/format_values.h"
#include "ui/text/text_entity.h"
#include "apiwrap.h"
#include "api/api_text_entities.h"
#include "core/core_cloud_password.h"
#include "window/themes/window_theme.h"
#include "webview/webview_interface.h"
@ -120,6 +121,8 @@ not_null<Main::Session*> SessionFromId(const InvoiceId &id) {
return slug->session;
} else if (const auto slug = std::get_if<InvoiceCredits>(&id.value)) {
return slug->session;
} else if (const auto gift = std::get_if<InvoiceStarGift>(&id.value)) {
return &gift->user->session();
}
const auto &giftCode = v::get<InvoicePremiumGiftCode>(id.value);
const auto users = std::get_if<InvoicePremiumGiftCodeUsers>(
@ -376,6 +379,19 @@ MTPInputInvoice Form::inputInvoice() const {
MTP_long(credits->credits),
MTP_string(credits->currency),
MTP_long(credits->amount)));
} else if (const auto gift = std::get_if<InvoiceStarGift>(&_id.value)) {
using Flag = MTPDinputInvoiceStarGift::Flag;
return MTP_inputInvoiceStarGift(
MTP_flags((gift->anonymous ? Flag::f_hide_name : Flag(0))
| (gift->message.empty() ? Flag(0) : Flag::f_message)),
gift->user->inputUser,
MTP_long(gift->giftId),
MTP_textWithEntities(
MTP_string(gift->message.text),
Api::EntitiesToMTP(
&gift->user->session(),
gift->message.entities,
Api::ConvertOption::SkipLocal)));
}
const auto &giftCode = v::get<InvoicePremiumGiftCode>(_id.value);
if (giftCode.creditsAmount) {
@ -461,7 +477,31 @@ void Form::requestForm() {
};
_updates.fire(CreditsPaymentStarted{ .data = formData });
}, [&](const MTPDpayments_paymentFormStarGift &data) {
// todo pay for star gift.
const auto currency = qs(data.vinvoice().data().vcurrency());
const auto &tlPrices = data.vinvoice().data().vprices().v;
const auto amount = tlPrices.empty()
? 0
: tlPrices.front().data().vamount().v;
if (currency != ::Ui::kCreditsCurrency || !amount) {
using Type = Error::Type;
_updates.fire(Error{ Type::Form, u"Bad Stars Form."_q });
return;
}
const auto invoice = InvoiceCredits{
.session = _session,
.randomId = 0,
.credits = amount,
.currency = currency,
.amount = amount,
};
const auto formData = CreditsFormData{
.id = _id,
.formId = data.vform_id().v,
.invoice = invoice,
.inputInvoice = inputInvoice(),
.starGiftForm = true,
};
_updates.fire(CreditsPaymentStarted{ .data = formData });
});
}).fail([=](const MTP::Error &error) {
hideProgress();

View file

@ -171,12 +171,21 @@ struct InvoiceCredits {
PeerId giftPeerId = PeerId(0);
};
struct InvoiceStarGift {
uint64 giftId = 0;
uint64 randomId = 0;
TextWithEntities message;
not_null<UserData*> user;
bool anonymous = false;
};
struct InvoiceId {
std::variant<
InvoiceMessage,
InvoiceSlug,
InvoicePremiumGiftCode,
InvoiceCredits> value;
InvoiceCredits,
InvoiceStarGift> value;
};
struct CreditsFormData {
@ -188,6 +197,7 @@ struct CreditsFormData {
PhotoData *photo = nullptr;
InvoiceCredits invoice;
MTPInputInvoice inputInvoice;
bool starGiftForm = false;
};
struct CreditsReceiptData {

View file

@ -40,10 +40,10 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
}
void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) {
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Blocked) {
if (const auto onstack = maybeReturnToBot) {
@ -55,6 +55,20 @@ void ProcessCreditsPayment(
onstack(CheckoutResult::Cancelled);
}
return;
} else if (form->starGiftForm) {
const auto done = [=](std::optional<QString> error) {
const auto onstack = maybeReturnToBot;
if (error) {
show->showToast(*error);
if (onstack) {
onstack(CheckoutResult::Failed);
}
} else if (onstack) {
onstack(CheckoutResult::Paid);
}
};
Ui::SendStarGift(&show->session(), form, done);
return;
}
const auto unsuccessful = std::make_shared<bool>(true);
const auto box = show->show(Box(
@ -65,14 +79,16 @@ void ProcessCreditsPayment(
if (const auto widget = fireworks.data()) {
Ui::StartFireworks(widget);
}
if (maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Paid);
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Paid);
}
}));
box->boxClosing() | rpl::start_with_next([=] {
crl::on_main([=] {
if ((*unsuccessful) && maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Cancelled);
if (*unsuccessful) {
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Cancelled);
}
}
});
}, box->lifetime());