Improve bot checkout error messages.

This commit is contained in:
John Preston 2024-07-18 16:41:18 +02:00
parent 484c647b5b
commit f8b756d447
13 changed files with 221 additions and 120 deletions

View file

@ -3161,6 +3161,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time.";
"lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
"lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
"lng_bot_click_to_start" = "Click here to use this bot.";
"lng_typing" = "typing";
"lng_user_typing" = "{user} is typing";
@ -3757,6 +3758,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_card_declined" = "Your card was declined.";
"lng_payments_payment_failed" = "Payment failed. Your card has not been billed.";
"lng_payments_precheckout_failed" = "The bot couldn't process your payment. Your card has not been billed.";
"lng_payments_precheckout_timeout" = "The bot didn't respond in time. Your card has not been billed.";
"lng_payments_precheckout_stars_failed" = "The bot couldn't process your payment.";
"lng_payments_precheckout_stars_timeout" = "The bot didn't respond in time.";
"lng_payments_already_paid" = "You have already paid for this item.";
"lng_payments_terms_title" = "Terms of Service";

View file

@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/payments_checkout_process.h"
#include "payments/payments_form.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg.
@ -257,6 +258,8 @@ void SendCreditsBox(
if (state->confirmButtonBusy.current()) {
return;
}
const auto show = box->uiShow();
const auto weak = MakeWeak(box.get());
state->confirmButtonBusy = true;
session->api().request(
MTPpayments_SendStarsForm(
@ -264,12 +267,31 @@ void SendCreditsBox(
MTP_long(form->formId),
form->inputInvoice)
).done([=](auto result) {
state->confirmButtonBusy = false;
box->closeBox();
if (weak) {
state->confirmButtonBusy = false;
box->closeBox();
}
sent();
}).fail([=](const MTP::Error &error) {
state->confirmButtonBusy = false;
box->uiShow()->showToast(error.type());
if (weak) {
state->confirmButtonBusy = false;
}
const auto id = error.type();
if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
auto error = ::Ui::MakeInformBox(
tr::lng_payments_precheckout_stars_failed(tr::now));
error->boxClosing() | rpl::start_with_next([=] {
if (const auto paybox = weak.data()) {
paybox->closeBox();
}
}, error->lifetime());
show->showBox(std::move(error));
} else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
show->showToast(
tr::lng_payments_precheckout_stars_timeout(tr::now));
} else {
show->showToast(id);
}
}).send();
});
{

View file

@ -241,19 +241,20 @@ ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
}
}
});
const auto reactivate = controller
? crl::guard(
controller,
[=](auto) { controller->widget()->activate(); })
: Fn<void(Payments::CheckoutResult)>();
const auto credits = Payments::IsCreditsInvoice(item);
const auto nonPanelPaymentFormProcess = (controller && credits)
? Payments::ProcessNonPanelPaymentFormFactory(controller, done)
: nullptr;
Payments::CheckoutProcess::Start(
item,
Payments::Mode::Payment,
(controller
? crl::guard(
controller,
[=](auto) { controller->widget()->activate(); })
: Fn<void(Payments::CheckoutResult)>()),
((controller && Payments::IsCreditsInvoice(item))
? Payments::ProcessNonPanelPaymentFormFactory(
controller,
done)
: nullptr));
reactivate,
nonPanelPaymentFormProcess);
});
}

View file

@ -621,16 +621,38 @@ void AttachWebView::botHandleInvoice(QString slug) {
}());
}
};
_panel->hideForPayment();
Payments::CheckoutProcess::Start(
&_bot->session(),
slug,
reactivate,
_context
? Payments::ProcessNonPanelPaymentFormFactory(
_context->controller.get(),
reactivate)
: nullptr);
nonPanelPaymentFormFactory(reactivate));
}
auto AttachWebView::nonPanelPaymentFormFactory(
Fn<void(Payments::CheckoutResult)> reactivate)
-> Fn<void(Payments::NonPanelPaymentForm)> {
using namespace Payments;
const auto panel = base::make_weak(_panel.get());
const auto weak = _context ? _context->controller : nullptr;
return [=](Payments::NonPanelPaymentForm form) {
using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
v::match(form, [&](const CreditsFormDataPtr &form) {
if (const auto strong = panel.get()) {
ProcessCreditsPayment(
uiShow(),
strong->toastParent().get(),
form,
reactivate);
}
}, [&](const CreditsReceiptPtr &receipt) {
if (const auto controller = weak.get()) {
ProcessCreditsReceipt(controller, receipt, reactivate);
}
}, [&](RealFormPresentedNotification) {
_panel->hideForPayment();
});
};
}
void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {

View file

@ -40,6 +40,11 @@ namespace Data {
class DocumentMedia;
} // namespace Data
namespace Payments {
struct NonPanelPaymentForm;
enum class CheckoutResult;
} // namespace Payments
namespace InlineBots {
enum class PeerType : uint8 {
@ -246,6 +251,8 @@ private:
void showToast(
const QString &text,
Window::SessionController *controller = nullptr);
Fn<void(Payments::NonPanelPaymentForm)> nonPanelPaymentFormFactory(
Fn<void(Payments::CheckoutResult)> reactivate);
const not_null<Main::Session*> _session;

View file

@ -536,6 +536,8 @@ void CheckoutProcess::handleError(const Error &error) {
showToast({ tr::lng_payments_payment_failed(tr::now) });
} else if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
showToast({ tr::lng_payments_precheckout_failed(tr::now) });
} else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
showToast({ tr::lng_payments_precheckout_timeout(tr::now) });
} else if (id == u"REQUESTED_INFO_INVALID"_q
|| id == u"SHIPPING_OPTION_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_INVALID"_q
@ -764,6 +766,14 @@ void CheckoutProcess::showForm() {
_form->information(),
_form->paymentMethod().ui,
_form->shippingOptions());
if (_nonPanelPaymentFormProcess && !_realFormNotified) {
_realFormNotified = true;
const auto weak = base::make_weak(_panel.get());
_nonPanelPaymentFormProcess(RealFormPresentedNotification());
if (weak) {
requestActivate();
}
}
}
void CheckoutProcess::showEditInformation(Ui::InformationField field) {

View file

@ -55,9 +55,13 @@ enum class CheckoutResult {
Failed,
};
struct NonPanelPaymentForm : std::variant<
std::shared_ptr<CreditsFormData>,
std::shared_ptr<CreditsReceiptData>> {
struct RealFormPresentedNotification {
};
struct NonPanelPaymentForm
: std::variant<
std::shared_ptr<CreditsFormData>,
std::shared_ptr<CreditsReceiptData>,
RealFormPresentedNotification> {
using variant::variant;
};
@ -183,6 +187,7 @@ private:
Fn<void(NonPanelPaymentForm)> _nonPanelPaymentFormProcess;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;
bool _realFormNotified = false;
bool _sendFormPending = false;
bool _sendFormFailed = false;

View file

@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/boost_box.h" // Ui::StartFireworks.
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
namespace Payments {
@ -37,84 +38,98 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
return invoice && (invoice->currency == Ui::kCreditsCurrency);
}
void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto api = lifetime->make_state<Api::CreditsStatus>(
show->session().user());
const auto sendBox = [=] {
const auto unsuccessful = std::make_shared<bool>(true);
const auto box = show->show(Box(
Ui::SendCreditsBox,
form,
[=] {
*unsuccessful = false;
if (const auto widget = fireworks.data()) {
Ui::StartFireworks(widget);
}
if (maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Paid);
}
}));
box->boxClosing() | rpl::start_with_next([=] {
crl::on_main([=] {
if ((*unsuccessful) && maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Cancelled);
}
});
}, box->lifetime());
};
api->request({}, [=](Data::CreditsStatusSlice slice) {
show->session().setCredits(slice.balance);
const auto creditsNeeded = int64(form->invoice.credits)
- int64(slice.balance);
if (creditsNeeded <= 0) {
sendBox();
} else if (show->session().premiumPossible()) {
show->show(Box(
Settings::SmallBalanceBox,
show,
creditsNeeded,
form->botId,
sendBox));
} else {
show->showToast(
tr::lng_credits_purchase_blocked(tr::now));
if (maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Failed);
}
}
lifetime->destroy();
});
}
void ProcessCreditsReceipt(
not_null<Window::SessionController*> controller,
std::shared_ptr<CreditsReceiptData> receipt,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto entry = Data::CreditsHistoryEntry{
.id = receipt->id,
.title = receipt->title,
.description = receipt->description,
.date = base::unixtime::parse(receipt->date),
.photoId = receipt->photo ? receipt->photo->id : 0,
.credits = receipt->credits,
.bareMsgId = uint64(),
.barePeerId = receipt->peerId.value,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
};
controller->uiShow()->show(Box(
Settings::ReceiptCreditsBox,
controller,
nullptr,
entry));
controller->window().activate();
}
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
Fn<void(CheckoutResult)> maybeReturnToBot) {
return [=](NonPanelPaymentForm form) {
using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
if (const auto creditsData = std::get_if<CreditsFormDataPtr>(&form)) {
const auto form = *creditsData;
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto api = lifetime->make_state<Api::CreditsStatus>(
controller->session().user());
const auto sendBox = [=, weak = base::make_weak(controller)] {
if (const auto strong = weak.get()) {
const auto unsuccessful = std::make_shared<bool>(true);
const auto box = controller->uiShow()->show(Box(
Ui::SendCreditsBox,
form,
crl::guard(strong, [=] {
*unsuccessful = false;
Ui::StartFireworks(strong->content());
if (maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Paid);
}
})));
box->boxClosing() | rpl::start_with_next([=] {
crl::on_main([=] {
if ((*unsuccessful) && maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Cancelled);
}
});
}, box->lifetime());
}
};
const auto weak = base::make_weak(controller);
api->request({}, [=](Data::CreditsStatusSlice slice) {
if (const auto strong = weak.get()) {
strong->session().setCredits(slice.balance);
const auto creditsNeeded = int64(form->invoice.credits)
- int64(slice.balance);
if (creditsNeeded <= 0) {
sendBox();
} else if (strong->session().premiumPossible()) {
strong->uiShow()->show(Box(
Settings::SmallBalanceBox,
strong,
creditsNeeded,
form->botId,
sendBox));
} else {
strong->uiShow()->showToast(
tr::lng_credits_purchase_blocked(tr::now));
if (maybeReturnToBot) {
maybeReturnToBot(CheckoutResult::Failed);
}
}
}
lifetime->destroy();
});
}
if (const auto r = std::get_if<CreditsReceiptPtr>(&form)) {
const auto receipt = *r;
const auto entry = Data::CreditsHistoryEntry{
.id = receipt->id,
.title = receipt->title,
.description = receipt->description,
.date = base::unixtime::parse(receipt->date),
.photoId = receipt->photo ? receipt->photo->id : 0,
.credits = receipt->credits,
.bareMsgId = uint64(),
.barePeerId = receipt->peerId.value,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
};
controller->uiShow()->show(Box(
Settings::ReceiptCreditsBox,
controller,
nullptr,
entry));
}
v::match(form, [&](const CreditsFormDataPtr &form) {
ProcessCreditsPayment(
controller->uiShow(),
controller->content().get(),
form,
maybeReturnToBot);
}, [&](const CreditsReceiptPtr &receipt) {
ProcessCreditsReceipt(controller, receipt, maybeReturnToBot);
}, [](RealFormPresentedNotification) {});
};
}

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
namespace Main {
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
@ -16,10 +20,23 @@ class SessionController;
namespace Payments {
enum class CheckoutResult;
struct CreditsFormData;
struct CreditsReceiptData;
struct NonPanelPaymentForm;
[[nodiscard]] 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 = nullptr);
void ProcessCreditsReceipt(
not_null<Window::SessionController*> controller,
std::shared_ptr<CreditsReceiptData> receipt,
Fn<void(CheckoutResult)> maybeReturnToBot = nullptr);
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
Fn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr);

View file

@ -289,7 +289,7 @@ void Credits::setupContent() {
Ui::StartFireworks(_parent);
}
};
FillCreditOptions(_controller, content, 0, paid);
FillCreditOptions(_controller->uiShow(), content, 0, paid);
setupHistory(content);
Ui::ResizeFitChild(this, content);

View file

@ -225,7 +225,7 @@ void AddViewMediaHandler(
} // namespace
void FillCreditOptions(
not_null<Window::SessionController*> controller,
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::VerticalLayout*> container,
int minimumCredits,
Fn<void()> paid) {
@ -302,7 +302,7 @@ void FillCreditOptions(
}, button->lifetime());
button->setClickedCallback([=] {
const auto invoice = Payments::InvoiceCredits{
.session = &controller->session(),
.session = &show->session(),
.randomId = UniqueIdFromOption(option),
.credits = option.credits,
.product = option.product,
@ -348,18 +348,18 @@ void FillCreditOptions(
using ApiOptions = Api::CreditsTopupOptions;
const auto apiCredits = content->lifetime().make_state<ApiOptions>(
controller->session().user());
show->session().user());
if (controller->session().premiumPossible()) {
if (show->session().premiumPossible()) {
apiCredits->request(
) | rpl::start_with_error_done([=](const QString &error) {
controller->showToast(error);
show->showToast(error);
}, [=] {
fill(apiCredits->options());
}, content->lifetime());
}
controller->session().premiumPossibleValue(
show->session().premiumPossibleValue(
) | rpl::start_with_next([=](bool premiumPossible) {
if (!premiumPossible) {
fill({});
@ -739,7 +739,7 @@ object_ptr<Ui::RpWidget> PaidMediaThumbnail(
void SmallBalanceBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
std::shared_ptr<Main::SessionShow> show,
int creditsNeeded,
UserId botId,
Fn<void()> paid) {
@ -750,21 +750,13 @@ void SmallBalanceBox(
paid();
};
const auto bot = controller->session().data().user(botId).get();
const auto bot = show->session().data().user(botId).get();
const auto content = [&]() -> Ui::Premium::TopBarAbstract* {
const auto weak = base::make_weak(controller);
const auto clickContextOther = [=] {
return QVariant::fromValue(ClickHandlerContext{
.sessionWindow = weak,
.botStartAutoSubmit = true,
});
};
return box->setPinnedToTopContent(object_ptr<Ui::Premium::TopBar>(
box,
st::creditsLowBalancePremiumCover,
Ui::Premium::TopBarDescriptor{
.clickContextOther = clickContextOther,
.title = tr::lng_credits_small_balance_title(
lt_count,
rpl::single(creditsNeeded) | tr::to_count()),
@ -777,7 +769,7 @@ void SmallBalanceBox(
}));
}();
FillCreditOptions(controller, box->verticalLayout(), creditsNeeded, done);
FillCreditOptions(show, box->verticalLayout(), creditsNeeded, done);
content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight);
content->setMinimumHeight(st::infoLayerTopBarHeight);
@ -796,12 +788,12 @@ void SmallBalanceBox(
{
const auto balance = AddBalanceWidget(
content,
controller->session().creditsValue(),
show->session().creditsValue(),
true);
const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
controller->session().user());
show->session().user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
controller->session().setCredits(slice.balance);
show->session().setCredits(slice.balance);
});
rpl::combine(
balance->sizeValue(),

View file

@ -16,6 +16,10 @@ namespace Data {
struct CreditsHistoryEntry;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
@ -29,7 +33,7 @@ class VerticalLayout;
namespace Settings {
void FillCreditOptions(
not_null<Window::SessionController*> controller,
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::VerticalLayout*> container,
int minCredits,
Fn<void()> paid);
@ -77,7 +81,7 @@ void ShowRefundInfoBox(
void SmallBalanceBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
std::shared_ptr<Main::SessionShow> show,
int creditsNeeded,
UserId botId,
Fn<void()> paid);

View file

@ -1344,8 +1344,10 @@ void Panel::invoiceClosed(const QString &slug, const QString &status) {
{ u"slug"_q, slug },
{ u"status"_q, status },
});
_widget->showAndActivate();
_hiddenForPayment = false;
if (_hiddenForPayment) {
_hiddenForPayment = false;
_widget->showAndActivate();
}
}
void Panel::hideForPayment() {