diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 6a1d8c1b1..5b4a8cce1 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -733,7 +733,7 @@ bool ResolveInvoice( Payments::CheckoutProcess::Start( &controller->session(), slug, - crl::guard(window, [=] { window->activate(); })); + crl::guard(window, [=](auto) { window->activate(); })); return true; } diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 1409b6deb..3a0c749a9 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -120,7 +120,7 @@ void activateBotCommand( Payments::CheckoutProcess::Start( msg, Payments::Mode::Payment, - crl::guard(App::wnd(), [] { App::wnd()->activate(); })); + crl::guard(App::wnd(), [](auto) { App::wnd()->activate(); })); } break; case ButtonType::Url: { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 722f4cd48..b043f2e6e 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -1032,24 +1032,31 @@ void History::applyServiceChanges( } }, [&](const MTPDmessageActionPaymentSent &data) { if (const auto payment = item->Get()) { + auto paid = std::optional(); if (const auto message = payment->msg) { if (const auto media = message->media()) { if (const auto invoice = media->invoice()) { - using Payments::CheckoutProcess; - if (CheckoutProcess::TakePaymentStarted(message)) { - // Toast on a current active window. - Ui::ShowMultilineToast({ - .text = tr::lng_payments_success( - tr::now, - lt_amount, - Ui::Text::Bold(payment->amount), - lt_title, - Ui::Text::Bold(invoice->title), - Ui::Text::WithEntities), - }); - } + paid = Payments::CheckoutProcess::InvoicePaid( + message); } } + } else if (!payment->slug.isEmpty()) { + using Payments::CheckoutProcess; + paid = Payments::CheckoutProcess::InvoicePaid( + &session(), + payment->slug); + } + if (paid) { + // Toast on a current active window. + Ui::ShowMultilineToast({ + .text = tr::lng_payments_success( + tr::now, + lt_amount, + Ui::Text::Bold(payment->amount), + lt_title, + Ui::Text::Bold(paid->title), + Ui::Text::WithEntities), + }); } } }, [&](const MTPDmessageActionSetChatTheme &data) { diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 8ec617fdf..285bef45a 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -1372,7 +1372,7 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { CheckoutProcess::Start( item, Mode::Receipt, - crl::guard(weak, [=] { weak->window().activate(); })); + crl::guard(weak, [=](auto) { weak->window().activate(); })); } }); } else if (type == mtpc_messageActionGroupCall diff --git a/Telegram/SourceFiles/history/history_service.h b/Telegram/SourceFiles/history/history_service.h index 09278b230..380d8696f 100644 --- a/Telegram/SourceFiles/history/history_service.h +++ b/Telegram/SourceFiles/history/history_service.h @@ -34,6 +34,7 @@ struct HistoryServiceGameScore struct HistoryServicePayment : public RuntimeComponent , public HistoryServiceDependentData { + QString slug; QString amount; ClickHandlerPtr invoiceLink; }; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 3a42643e3..1db3f2b0e 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/basic_click_handlers.h" #include "history/history.h" #include "history/history_item.h" +#include "payments/payments_checkout_process.h" #include "storage/storage_account.h" #include "lang/lang_keys.h" #include "base/random.h" @@ -711,6 +712,28 @@ void AttachWebView::show( close(); return true; }; + const auto panel = std::make_shared< + base::weak_ptr>(nullptr); + const auto handleInvoice = [=, session = _session](QString slug) { + using Result = Payments::CheckoutResult; + const auto reactivate = [=](Result result) { + if (const auto strong = panel->get()) { + strong->invoiceClosed(slug, [&] { + switch (result) { + case Result::Paid: return "paid"; + case Result::Failed: return "failed"; + case Result::Pending: return "pending"; + case Result::Cancelled: return "cancelled"; + } + Unexpected("Payments::CheckoutResult value."); + }()); + } + }; + if (const auto strong = panel->get()) { + strong->hideForPayment(); + } + Payments::CheckoutProcess::Start(session, slug, reactivate); + }; auto title = Info::Profile::NameValue( _bot ) | rpl::map([](const TextWithEntities &value) { @@ -723,10 +746,12 @@ void AttachWebView::show( .title = std::move(title), .bottom = rpl::single('@' + _bot->username), .handleLocalUri = handleLocalUri, + .handleInvoice = handleInvoice, .sendData = sendData, .close = close, .themeParams = [] { return Window::Theme::WebViewParams(); }, }); + *panel = _panel.get(); started(queryId); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 6af773d59..307e96465 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -37,8 +37,8 @@ namespace { struct SessionProcesses { base::flat_map> byItem; base::flat_map> bySlug; - base::flat_set paymentStartedByItem; - base::flat_set paymentStartedBySlug; + base::flat_map paymentStartedByItem; + base::flat_map paymentStartedBySlug; rpl::lifetime lifetime; }; @@ -64,7 +64,7 @@ base::flat_map, SessionProcesses> Processes; void CheckoutProcess::Start( not_null item, Mode mode, - Fn reactivate) { + Fn reactivate) { auto &processes = LookupSessionProcesses(&item->history()->session()); const auto media = item->media(); const auto invoice = media ? media->invoice() : nullptr; @@ -99,7 +99,7 @@ void CheckoutProcess::Start( void CheckoutProcess::Start( not_null session, const QString &slug, - Fn reactivate) { + Fn reactivate) { auto &processes = LookupSessionProcesses(session); const auto i = processes.bySlug.find(slug); if (i != end(processes.bySlug)) { @@ -117,24 +117,57 @@ void CheckoutProcess::Start( j->second->requestActivate(); } -bool CheckoutProcess::TakePaymentStarted( +std::optional CheckoutProcess::InvoicePaid( not_null item) { const auto session = &item->history()->session(); const auto itemId = item->fullId(); const auto i = Processes.find(session); - if (i == end(Processes) - || !i->second.paymentStartedByItem.contains(itemId)) { - return false; + if (i == end(Processes)) { + return std::nullopt; } - i->second.paymentStartedByItem.erase(itemId); + const auto k = i->second.paymentStartedByItem.find(itemId); + if (k == end(i->second.paymentStartedByItem)) { + return std::nullopt; + } + const auto result = k->second; + i->second.paymentStartedByItem.erase(k); + const auto j = i->second.byItem.find(itemId); if (j != end(i->second.byItem)) { - j->second->closeAndReactivate(); + j->second->closeAndReactivate(CheckoutResult::Paid); } else if (i->second.paymentStartedByItem.empty() - && i->second.byItem.empty()) { + && i->second.byItem.empty() + && i->second.paymentStartedBySlug.empty() + && i->second.bySlug.empty()) { Processes.erase(i); } - return true; + return result; +} + +std::optional CheckoutProcess::InvoicePaid( + not_null session, + const QString &slug) { + const auto i = Processes.find(session); + if (i == end(Processes)) { + return std::nullopt; + } + const auto k = i->second.paymentStartedBySlug.find(slug); + if (k == end(i->second.paymentStartedBySlug)) { + return std::nullopt; + } + const auto result = k->second; + i->second.paymentStartedBySlug.erase(k); + + const auto j = i->second.bySlug.find(slug); + if (j != end(i->second.bySlug)) { + j->second->closeAndReactivate(CheckoutResult::Paid); + } else if (i->second.paymentStartedByItem.empty() + && i->second.byItem.empty() + && i->second.paymentStartedBySlug.empty() + && i->second.bySlug.empty()) { + Processes.erase(i); + } + return result; } void CheckoutProcess::ClearAll() { @@ -142,18 +175,19 @@ void CheckoutProcess::ClearAll() { } void CheckoutProcess::RegisterPaymentStart( - not_null process) { + not_null process, + PaidInvoice info) { const auto i = Processes.find(process->_session); Assert(i != end(Processes)); for (const auto &[itemId, itemProcess] : i->second.byItem) { if (itemProcess.get() == process) { - i->second.paymentStartedByItem.emplace(itemId); + i->second.paymentStartedByItem.emplace(itemId, info); return; } } for (const auto &[slug, itemProcess] : i->second.bySlug) { if (itemProcess.get() == process) { - i->second.paymentStartedBySlug.emplace(slug); + i->second.paymentStartedBySlug.emplace(slug, info); return; } } @@ -162,26 +196,33 @@ void CheckoutProcess::RegisterPaymentStart( void CheckoutProcess::UnregisterPaymentStart( not_null process) { const auto i = Processes.find(process->_session); - if (i != end(Processes)) { - for (const auto &[itemId, itemProcess] : i->second.byItem) { - if (itemProcess.get() == process) { - i->second.paymentStartedByItem.emplace(itemId); - return; - } + if (i == end(Processes)) { + return; + } + for (const auto &[itemId, itemProcess] : i->second.byItem) { + if (itemProcess.get() == process) { + i->second.paymentStartedByItem.remove(itemId); + break; } - for (const auto &[slug, itemProcess] : i->second.bySlug) { - if (itemProcess.get() == process) { - i->second.paymentStartedBySlug.emplace(slug); - return; - } + } + for (const auto &[slug, itemProcess] : i->second.bySlug) { + if (itemProcess.get() == process) { + i->second.paymentStartedBySlug.remove(slug); + break; } } + if (i->second.paymentStartedByItem.empty() + && i->second.byItem.empty() + && i->second.paymentStartedBySlug.empty() + && i->second.bySlug.empty()) { + Processes.erase(i); + } } CheckoutProcess::CheckoutProcess( InvoiceId id, Mode mode, - Fn reactivate, + Fn reactivate, PrivateTag) : _session(SessionFromId(id)) , _form(std::make_unique
(id, (mode == Mode::Receipt))) @@ -221,7 +262,8 @@ CheckoutProcess::CheckoutProcess( CheckoutProcess::~CheckoutProcess() { } -void CheckoutProcess::setReactivateCallback(Fn reactivate) { +void CheckoutProcess::setReactivateCallback( + Fn reactivate) { _reactivate = std::move(reactivate); } @@ -276,6 +318,8 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { auto bottomText = tr::lng_payments_processed_by( lt_provider, rpl::single(_form->invoice().provider)); + _sendFormFailed = false; + _sendFormPending = true; if (!_panel->showWebview(data.url, false, std::move(bottomText))) { File::OpenUrl(data.url); close(); @@ -284,7 +328,7 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { const auto weak = base::make_weak(this); _session->api().applyUpdates(data.updates); if (weak) { - closeAndReactivate(); + closeAndReactivate(CheckoutResult::Paid); } }, [&](const Error &error) { handleError(error); @@ -386,6 +430,7 @@ void CheckoutProcess::handleError(const Error &error) { } break; case Error::Type::Send: + _sendFormFailed = true; if (const auto box = _enterPasswordBox.data()) { box->closeBox(); } @@ -424,14 +469,18 @@ void CheckoutProcess::panelRequestClose() { } void CheckoutProcess::panelCloseSure() { - closeAndReactivate(); + closeAndReactivate(_sendFormFailed + ? CheckoutResult::Failed + : _sendFormPending + ? CheckoutResult::Pending + : CheckoutResult::Cancelled); } -void CheckoutProcess::closeAndReactivate() { +void CheckoutProcess::closeAndReactivate(CheckoutResult result) { const auto reactivate = std::move(_reactivate); close(); if (reactivate) { - reactivate(); + reactivate(result); } } @@ -463,7 +512,7 @@ void CheckoutProcess::close() { void CheckoutProcess::panelSubmit() { if (_form->invoice().receipt.paid) { - closeAndReactivate(); + closeAndReactivate(CheckoutResult::Paid); return; } else if (_submitState == SubmitState::Validating || _submitState == SubmitState::Finishing) { @@ -485,7 +534,7 @@ void CheckoutProcess::panelSubmit() { } else if (!method.newCredentials && !method.savedCredentials) { editPaymentMethod(); } else { - RegisterPaymentStart(this); + RegisterPaymentStart(this, { _form->invoice().cover.title }); _submitState = SubmitState::Finishing; _form->submit(); } @@ -550,7 +599,8 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { if (Core::TryConvertUrlToLocal(uri) == uri) { return true; } - crl::on_main(this, [=] { closeAndReactivate(); }); + // #TODO payments + crl::on_main(this, [=] { closeAndReactivate(CheckoutResult::Paid); }); return false; } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 6abe173d6..74fdc667c 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -43,6 +43,17 @@ enum class Mode { Receipt, }; +enum class CheckoutResult { + Paid, + Pending, + Cancelled, + Failed, +}; + +struct PaidInvoice { + QString title; +}; + class CheckoutProcess final : public base::has_weak_ptr , private Ui::PanelDelegate { @@ -52,19 +63,22 @@ public: static void Start( not_null item, Mode mode, - Fn reactivate); + Fn reactivate); static void Start( not_null session, const QString &slug, - Fn reactivate); - [[nodiscard]] static bool TakePaymentStarted( + Fn reactivate); + [[nodiscard]] static std::optional InvoicePaid( not_null item); + [[nodiscard]] static std::optional InvoicePaid( + not_null session, + const QString &slug); static void ClearAll(); CheckoutProcess( InvoiceId id, Mode mode, - Fn reactivate, + Fn reactivate, PrivateTag); ~CheckoutProcess(); @@ -77,12 +91,14 @@ private: }; [[nodiscard]] not_null panelDelegate(); - static void RegisterPaymentStart(not_null process); + static void RegisterPaymentStart( + not_null process, + PaidInvoice info); static void UnregisterPaymentStart(not_null process); - void setReactivateCallback(Fn reactivate); + void setReactivateCallback(Fn reactivate); void requestActivate(); - void closeAndReactivate(); + void closeAndReactivate(CheckoutResult result); void close(); void handleFormUpdate(const FormUpdate &update); @@ -137,9 +153,11 @@ private: const std::unique_ptr _form; const std::unique_ptr _panel; QPointer _enterPasswordBox; - Fn _reactivate; + Fn _reactivate; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; + bool _sendFormPending = false; + bool _sendFormFailed = false; bool _themeUpdateScheduled = false; rpl::lifetime _gettingPasswordState; diff --git a/Telegram/SourceFiles/storage/download_manager_mtproto.cpp b/Telegram/SourceFiles/storage/download_manager_mtproto.cpp index ce6f3c9f2..cc154b0b6 100644 --- a/Telegram/SourceFiles/storage/download_manager_mtproto.cpp +++ b/Telegram/SourceFiles/storage/download_manager_mtproto.cpp @@ -673,7 +673,7 @@ void DownloadMtprotoTask::cdnPartLoaded(const MTPupload_CdnFile &result, mtpRequ auto ivec = bytes::make_span(state.ivec); std::copy(iv.begin(), iv.end(), ivec.begin()); - auto counterOffset = static_cast(requestData.offset) >> 4; + auto counterOffset = static_cast(requestData.offset >> 4); state.ivec[15] = static_cast(counterOffset & 0xFF); state.ivec[14] = static_cast((counterOffset >> 8) & 0xFF); state.ivec[13] = static_cast((counterOffset >> 16) & 0xFF); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 1f9efff7b..70ebbd95f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -305,11 +305,13 @@ Panel::Panel( const QString &userDataPath, rpl::producer title, Fn handleLocalUri, + Fn handleInvoice, Fn sendData, Fn close, Fn themeParams) : _userDataPath(userDataPath) , _handleLocalUri(std::move(handleLocalUri)) +, _handleInvoice(std::move(handleInvoice)) , _sendData(std::move(sendData)) , _close(std::move(close)) , _widget(std::make_unique()) { @@ -320,7 +322,9 @@ Panel::Panel( ) | rpl::start_with_next(_close, _widget->lifetime()); _widget->closeEvents( - ) | rpl::start_with_next(_close, _widget->lifetime()); + ) | rpl::filter([=] { + return !_hiddenForPayment; + }) | rpl::start_with_next(_close, _widget->lifetime()); rpl::combine( style::PaletteChanged(), @@ -568,6 +572,10 @@ bool Panel::createWebview() { processMainButtonMessage(list.at(1)); } else if (command == "web_app_request_theme") { _themeUpdateForced.fire({}); + } else if (command == "web_app_open_tg_link") { + openTgLink(list.at(1).toString()); + } else if (command == "web_app_open_invoice") { + openInvoice(list.at(1).toString()); } }); @@ -618,6 +626,38 @@ void Panel::sendDataMessage(const QJsonValue &value) { _sendData(data.toUtf8()); } +void Panel::openTgLink(const QJsonValue &value) { + const auto json = value.toString(); + const auto args = ParseMethodArgs(json); + if (args.isEmpty()) { + _close(); + return; + } + const auto path = args["path_full"].toString(); + if (path.isEmpty()) { + LOG(("BotWebView Error: Bad tg link \"%1\".").arg(json)); + _close(); + return; + } + _handleLocalUri("https://t.me" + path); +} + +void Panel::openInvoice(const QJsonValue &value) { + const auto json = value.toString(); + const auto args = ParseMethodArgs(json); + if (args.isEmpty()) { + _close(); + return; + } + const auto slug = args["slug"].toString(); + if (slug.isEmpty()) { + LOG(("BotWebView Error: Bad invoice \"%1\".").arg(json)); + _close(); + return; + } + _handleInvoice(slug); +} + void Panel::processMainButtonMessage(const QJsonValue &value) { const auto json = value.toString(); const auto args = ParseMethodArgs(json); @@ -760,6 +800,22 @@ void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) { postEvent("theme_changed", "\"theme_params\": " + params.json); } +void Panel::invoiceClosed(const QString &slug, const QString &status) { + if (!_webview || !_webview->window.widget()) { + return; + } + postEvent( + "invoice_closed", + "\"slug\": \"" + slug + "\", \"status\": \"" + status + "\""); + _widget->showAndActivate(); + _hiddenForPayment = false; +} + +void Panel::hideForPayment() { + _hiddenForPayment = true; + _widget->hideGetDuration(); +} + void Panel::postEvent(const QString &event, const QString &data) { _webview->window.eval(R"( if (window.TelegramGameProxy) { @@ -820,6 +876,7 @@ std::unique_ptr Show(Args &&args) { args.userDataPath, std::move(args.title), std::move(args.handleLocalUri), + std::move(args.handleInvoice), std::move(args.sendData), std::move(args.close), std::move(args.themeParams)); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 85bd54813..78a14d123 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/object_ptr.h" +#include "base/weak_ptr.h" namespace Ui { class BoxContent; @@ -29,12 +30,13 @@ struct MainButtonArgs { QString text; }; -class Panel final { +class Panel final : public base::has_weak_ptr { public: Panel( const QString &userDataPath, rpl::producer title, Fn handleLocalUri, + Fn handleInvoice, Fn sendData, Fn close, Fn themeParams); @@ -57,6 +59,9 @@ public: void updateThemeParams(const Webview::ThemeParams ¶ms); + void hideForPayment(); + void invoiceClosed(const QString &slug, const QString &status); + [[nodiscard]] rpl::lifetime &lifetime(); private: @@ -70,6 +75,8 @@ private: void setTitle(rpl::producer title); void sendDataMessage(const QJsonValue &value); void processMainButtonMessage(const QJsonValue &value); + void openTgLink(const QJsonValue &value); + void openInvoice(const QJsonValue &value); void createMainButton(); void postEvent(const QString &event, const QString &data = {}); @@ -80,6 +87,7 @@ private: QString _userDataPath; Fn _handleLocalUri; + Fn _handleInvoice; Fn _sendData; Fn _close; std::unique_ptr _widget; @@ -93,6 +101,7 @@ private: rpl::lifetime _bgLifetime; bool _webviewProgress = false; bool _themeUpdateScheduled = false; + bool _hiddenForPayment = false; }; @@ -102,6 +111,7 @@ struct Args { rpl::producer title; rpl::producer bottom; Fn handleLocalUri; + Fn handleInvoice; Fn sendData; Fn close; Fn themeParams;