From f8b756d4470749a9a07d6112a118ee5e48e4e380 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 Jul 2024 16:41:18 +0200 Subject: [PATCH] Improve bot checkout error messages. --- Telegram/Resources/langs/lang.strings | 4 + .../SourceFiles/boxes/send_credits_box.cpp | 30 +++- .../view/media/history_view_media_common.cpp | 21 +-- .../inline_bots/bot_attach_web_view.cpp | 34 +++- .../inline_bots/bot_attach_web_view.h | 7 + .../payments/payments_checkout_process.cpp | 10 ++ .../payments/payments_checkout_process.h | 11 +- .../payments/payments_non_panel_process.cpp | 159 ++++++++++-------- .../payments/payments_non_panel_process.h | 17 ++ .../SourceFiles/settings/settings_credits.cpp | 2 +- .../settings/settings_credits_graphics.cpp | 32 ++-- .../settings/settings_credits_graphics.h | 8 +- .../ui/chat/attach/attach_bot_webview.cpp | 6 +- 13 files changed, 221 insertions(+), 120 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d3427dce9..6c9828189 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index 39d7983d7..a8771cc17 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -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(); }); { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index dd32fa00f..71b01fbf8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -241,19 +241,20 @@ ClickHandlerPtr MakePaidMediaLink(not_null item) { } } }); + const auto reactivate = controller + ? crl::guard( + controller, + [=](auto) { controller->widget()->activate(); }) + : Fn(); + 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()), - ((controller && Payments::IsCreditsInvoice(item)) - ? Payments::ProcessNonPanelPaymentFormFactory( - controller, - done) - : nullptr)); + reactivate, + nonPanelPaymentFormProcess); }); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index c42ecb3b9..c8e88aed7 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -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 reactivate) +-> Fn { + 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; + using CreditsReceiptPtr = std::shared_ptr; + 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) { diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 87648b07e..f9f59f1ae 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -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 nonPanelPaymentFormFactory( + Fn reactivate); const not_null _session; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index cbb435389..9a5daf9a4 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -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) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 3c7f112ca..e783bba58 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -55,9 +55,13 @@ enum class CheckoutResult { Failed, }; -struct NonPanelPaymentForm : std::variant< - std::shared_ptr, - std::shared_ptr> { +struct RealFormPresentedNotification { +}; +struct NonPanelPaymentForm + : std::variant< + std::shared_ptr, + std::shared_ptr, + RealFormPresentedNotification> { using variant::variant; }; @@ -183,6 +187,7 @@ private: Fn _nonPanelPaymentFormProcess; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; + bool _realFormNotified = false; bool _sendFormPending = false; bool _sendFormFailed = false; diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index 69cf7abde..b62987a82 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -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 item) { return invoice && (invoice->currency == Ui::kCreditsCurrency); } +void ProcessCreditsPayment( + std::shared_ptr show, + QPointer fireworks, + std::shared_ptr form, + Fn maybeReturnToBot) { + const auto lifetime = std::make_shared(); + const auto api = lifetime->make_state( + show->session().user()); + const auto sendBox = [=] { + const auto unsuccessful = std::make_shared(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 controller, + std::shared_ptr receipt, + Fn 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 ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot) { return [=](NonPanelPaymentForm form) { using CreditsFormDataPtr = std::shared_ptr; using CreditsReceiptPtr = std::shared_ptr; - if (const auto creditsData = std::get_if(&form)) { - const auto form = *creditsData; - const auto lifetime = std::make_shared(); - const auto api = lifetime->make_state( - controller->session().user()); - const auto sendBox = [=, weak = base::make_weak(controller)] { - if (const auto strong = weak.get()) { - const auto unsuccessful = std::make_shared(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(&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) {}); }; } diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h index e8ab9375c..53a31f81c 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.h +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h @@ -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 item); +void ProcessCreditsPayment( + std::shared_ptr show, + QPointer fireworks, + std::shared_ptr form, + Fn maybeReturnToBot = nullptr); + +void ProcessCreditsReceipt( + not_null controller, + std::shared_ptr receipt, + Fn maybeReturnToBot = nullptr); + Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot = nullptr); diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 5088ce62a..e80f82a62 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -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); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 282945d9e..1eaef4568 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -225,7 +225,7 @@ void AddViewMediaHandler( } // namespace void FillCreditOptions( - not_null controller, + std::shared_ptr show, not_null container, int minimumCredits, Fn 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( - 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 PaidMediaThumbnail( void SmallBalanceBox( not_null box, - not_null controller, + std::shared_ptr show, int creditsNeeded, UserId botId, Fn 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( 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( - 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(), diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 406bfabca..47d5323c7 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -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 controller, + std::shared_ptr show, not_null container, int minCredits, Fn paid); @@ -77,7 +81,7 @@ void ShowRefundInfoBox( void SmallBalanceBox( not_null box, - not_null controller, + std::shared_ptr show, int creditsNeeded, UserId botId, Fn paid); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index cb922a4ff..ae27e4938 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -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() {