diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a5ac9cdf6b..cb9eec67d5 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -815,6 +815,10 @@ PRIVATE passport/passport_panel_form.h passport/passport_panel_password.cpp passport/passport_panel_password.h + payments/payments_checkout_process.cpp + payments/payments_checkout_process.h + payments/payments_form.cpp + payments/payments_form.h platform/linux/linux_desktop_environment.cpp platform/linux/linux_desktop_environment.h platform/linux/linux_gdk_helper.cpp @@ -1014,8 +1018,6 @@ PRIVATE ui/widgets/level_meter.h ui/widgets/multi_select.cpp ui/widgets/multi_select.h - ui/widgets/separate_panel.cpp - ui/widgets/separate_panel.h ui/countryinput.cpp ui/countryinput.h ui/empty_userpic.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 99b4ff0847..0a2a9c6ae2 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1860,6 +1860,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_invoice_label_test" = "Test invoice"; "lng_payments_receipt_button" = "Receipt"; +"lng_payments_checkout_title" = "Checkout"; +"lng_payments_total_label" = "Total"; +"lng_payments_pay_amount" = "Pay {amount}"; +//"lng_payments_payment_method" = "Payment Method"; // #TODO payments native +"lng_payments_shipping_address" = "Shipping Information"; +"lng_payments_shipping_method" = "Shipping Method"; +"lng_payments_info_name" = "Name"; +"lng_payments_info_email" = "Email"; +"lng_payments_info_phone" = "Phone"; +"lng_payments_shipping_address_title" = "Shipping Address"; +"lng_payments_save_shipping_about" = "You can save your shipping information for future use."; +//"lng_payments_payment_card" = "Payment Card"; // #TODO payments native +//"lng_payments_cardholder_title" = "Cardholder"; +//"lng_payments_cardholder_about" = "Cardholder Name"; +//"lng_payments_billing_address" = "Billing Address"; +//"lng_payments_zip_code" = "Zip Code"; +//"lng_payments_save_payment_about" = "You can save your payment information for future use."; +"lng_payments_save_information" = "Save Information"; + "lng_call_status_incoming" = "is calling you..."; "lng_call_status_connecting" = "connecting..."; "lng_call_status_exchanging" = "exchanging encryption keys..."; diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index fab5b10705..d2cf492b1f 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -126,6 +126,10 @@ QString UiIntegration::timeFormat() { return cTimeFormat(); } +QWidget *UiIntegration::modalWindowParent() { + return Core::App().getModalParent(); +} + std::shared_ptr UiIntegration::createLinkHandler( const EntityLinkData &data, const std::any &context) { diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index 2aac9a0aa7..5bdc337a46 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -46,6 +46,8 @@ public: void startFontsEnd() override; QString timeFormat() override; + QWidget *modalWindowParent() override; + std::shared_ptr createLinkHandler( const EntityLinkData &data, const std::any &context) override; diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index e285a92444..31080b4858 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -31,154 +31,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "history/view/media/history_view_media.h" +#include "payments/payments_checkout_process.h" #include "data/data_session.h" #include "styles/style_chat.h" -#include "webview/webview_embed.h" -#include "webview/webview_interface.h" -#include "core/local_url_handlers.h" -#include "ui/widgets/window.h" -#include "ui/toast/toast.h" -#include -#include -#include -#include - -namespace Api { - -void GetPaymentForm(not_null msg) { - const auto msgId = msg->id; - const auto session = &msg->history()->session(); - session->api().request(MTPpayments_GetPaymentForm( - MTP_int(msgId) - )).done([=](const MTPpayments_PaymentForm &result) { - const auto window = new Ui::Window(); - window->setGeometry({ - style::ConvertScale(100), - style::ConvertScale(100), - style::ConvertScale(640), - style::ConvertScale(480) - }); - window->show(); - - window->events() | rpl::start_with_next([=](not_null e) { - if (e->type() == QEvent::Close) { - window->deleteLater(); - } - }, window->lifetime()); - - const auto body = window->body(); - body->paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - QPainter(body).fillRect(clip, st::windowBg); - }, body->lifetime()); - - const auto webview = Ui::CreateChild( - window, - window); - if (!webview->widget()) { - delete window; - Ui::show(Box( - tr::lng_payments_not_supported(tr::now))); - return; - } - - body->geometryValue( - ) | rpl::start_with_next([=](QRect geometry) { - webview->widget()->setGeometry(geometry); - }, body->lifetime()); - - webview->setMessageHandler([=](const QJsonDocument &message) { - if (!message.isArray()) { - LOG(("Payments Error: " - "Not an array received in buy_callback arguments.")); - return; - } - const auto list = message.array(); - if (list.at(0).toString() != "payment_form_submit") { - return; - } else if (!list.at(1).isString()) { - LOG(("Payments Error: " - "Not a string received in buy_callback result.")); - return; - } - - auto error = QJsonParseError(); - const auto document = QJsonDocument::fromJson( - list.at(1).toString().toUtf8(), - &error); - if (error.error != QJsonParseError::NoError) { - LOG(("Payments Error: " - "Failed to parse buy_callback arguments, error: %1." - ).arg(error.errorString())); - return; - } else if (!document.isObject()) { - LOG(("Payments Error: " - "Not an object decoded in buy_callback result.")); - return; - } - const auto root = document.object(); - const auto title = root.value("title").toString(); - const auto credentials = root.value("credentials"); - if (!credentials.isObject()) { - LOG(("Payments Error: " - "Not an object received in payment credentials.")); - return; - } - const auto serializedCredentials = QJsonDocument( - credentials.toObject() - ).toJson(QJsonDocument::Compact); - session->api().request(MTPpayments_SendPaymentForm( - MTP_flags(0), - MTP_int(msgId), - MTPstring(), // requested_info_id - MTPstring(), // shipping_option_id, - MTP_inputPaymentCredentials( - MTP_flags(0), - MTP_dataJSON(MTP_bytes(serializedCredentials))) - )).done([=](const MTPpayments_PaymentResult &result) { - result.match([&](const MTPDpayments_paymentResult &data) { - delete window; - App::wnd()->activate(); - session->api().applyUpdates(data.vupdates()); - }, [&](const MTPDpayments_paymentVerificationNeeded &data) { - webview->navigate(qs(data.vurl())); - }); - }).fail([=](const RPCError &error) { - delete window; - App::wnd()->activate(); - Ui::Toast::Show("payments.sendPaymentForm: " + error.type()); - }).send(); - }); - - webview->setNavigationHandler([=](const QString &uri) { - if (Core::TryConvertUrlToLocal(uri) == uri) { - return true; - } - window->deleteLater(); - App::wnd()->activate(); - return false; - }); - - webview->init(R"( -window.TelegramWebviewProxy = { - postEvent: function(eventType, eventData) { - if (window.external && window.external.invoke) { - window.external.invoke(JSON.stringify([eventType, eventData])); - } - } -};)"); - - const auto &data = result.c_payments_paymentForm(); - webview->navigate(qs(data.vurl())); - }).fail([=](const RPCError &error) { - App::wnd()->activate(); - Ui::Toast::Show("payments.getPaymentForm: " + error.type()); - }).send(); -} - -} // namespace Api - namespace { [[nodiscard]] MainWidget *CheckMainWidget(not_null session) { @@ -266,12 +122,7 @@ void activateBotCommand( } break; case ButtonType::Buy: { - if (Webview::Supported()) { - Api::GetPaymentForm(msg); - } else { - Ui::show(Box( - tr::lng_payments_not_supported(tr::now))); - } + Payments::CheckoutProcess::Start(msg); } break; case ButtonType::Url: { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp new file mode 100644 index 0000000000..916150a204 --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -0,0 +1,240 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/payments_checkout_process.h" + +#include "payments/payments_form.h" +#include "payments/ui/payments_panel.h" +#include "payments/ui/payments_webview.h" +#include "main/main_session.h" +#include "main/main_account.h" +#include "history/history_item.h" +#include "history/history.h" +#include "core/local_url_handlers.h" // TryConvertUrlToLocal. +#include "apiwrap.h" + +// #TODO payments errors +#include "mainwindow.h" +#include "ui/toast/toast.h" + +#include +#include +#include +#include + +namespace Payments { +namespace { + +struct SessionProcesses { + base::flat_map> map; + rpl::lifetime lifetime; +}; + +base::flat_map, SessionProcesses> Processes; + +[[nodiscard]] SessionProcesses &LookupSessionProcesses( + not_null item) { + const auto session = &item->history()->session(); + const auto i = Processes.find(session); + if (i != end(Processes)) { + return i->second; + } + const auto j = Processes.emplace(session).first; + auto &result = j->second; + session->account().sessionChanges( + ) | rpl::start_with_next([=] { + Processes.erase(session); + }, result.lifetime); + return result; +} + +} // namespace + +void CheckoutProcess::Start(not_null item) { + auto &processes = LookupSessionProcesses(item); + const auto session = &item->history()->session(); + const auto id = item->fullId(); + const auto i = processes.map.find(id); + if (i != end(processes.map)) { + i->second->requestActivate(); + return; + } + const auto j = processes.map.emplace( + id, + std::make_unique(session, id, PrivateTag{})).first; + j->second->requestActivate(); +} + +CheckoutProcess::CheckoutProcess( + not_null session, + FullMsgId itemId, + PrivateTag) +: _session(session) +, _form(std::make_unique
(session, itemId)) +, _panel(std::make_unique(panelDelegate())) { + _form->updates( + ) | rpl::start_with_next([=](const FormUpdate &update) { + handleFormUpdate(update); + }, _lifetime); +} + +CheckoutProcess::~CheckoutProcess() { +} + +void CheckoutProcess::requestActivate() { + _panel->requestActivate(); +} + +not_null CheckoutProcess::panelDelegate() { + return static_cast(this); +} + +void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { + v::match(update.data, [&](const FormReady &) { + _panel->showForm(_form->invoice()); + }, [&](const FormError &error) { + handleFormError(error); + }, [&](const SendError &error) { + handleSendError(error); + }, [&](const VerificationNeeded &info) { + if (_webviewWindow) { + _webviewWindow->navigate(info.url); + } else { + _webviewWindow = std::make_unique( + info.url, + panelDelegate()); + if (!_webviewWindow->shown()) { + // #TODO payments errors + } + } + }, [&](const PaymentFinished &result) { + const auto weak = base::make_weak(this); + _session->api().applyUpdates(result.updates); + if (weak) { + panelCloseSure(); + } + }); +} + +void CheckoutProcess::handleFormError(const FormError &error) { + // #TODO payments errors + const auto &type = error.type; + if (type == u"PROVIDER_ACCOUNT_INVALID"_q) { + + } else if (type == u"PROVIDER_ACCOUNT_TIMEOUT"_q) { + + } else if (type == u"INVOICE_ALREADY_PAID"_q) { + + } + App::wnd()->activate(); + Ui::Toast::Show("payments.getPaymentForm: " + type); +} + +void CheckoutProcess::handleSendError(const SendError &error) { + // #TODO payments errors + const auto &type = error.type; + if (type == u"REQUESTED_INFO_INVALID"_q) { + + } else if (type == u"SHIPPING_OPTION_INVALID"_q) { + + } else if (type == u"PAYMENT_FAILED"_q) { + + } else if (type == u"PAYMENT_CREDENTIALS_INVALID"_q) { + + } else if (type == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) { + + } else if (type == u"BOT_PRECHECKOUT_FAILED"_q) { + + } + App::wnd()->activate(); + Ui::Toast::Show("payments.sendPaymentForm: " + type); +} + +void CheckoutProcess::panelRequestClose() { + panelCloseSure(); // #TODO payments confirmation +} + +void CheckoutProcess::panelCloseSure() { + const auto i = Processes.find(_session); + if (i == end(Processes)) { + return; + } + const auto j = ranges::find(i->second.map, this, [](const auto &pair) { + return pair.second.get(); + }); + if (j == end(i->second.map)) { + return; + } + i->second.map.erase(j); + if (i->second.map.empty()) { + Processes.erase(i); + } +} + +void CheckoutProcess::panelSubmit() { + _webviewWindow = std::make_unique( + _form->details().url, + panelDelegate()); + if (!_webviewWindow->shown()) { + // #TODO payments errors + } +} + +void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) { + if (!message.isArray()) { + LOG(("Payments Error: " + "Not an array received in buy_callback arguments.")); + return; + } + const auto list = message.array(); + if (list.at(0).toString() != "payment_form_submit") { + return; + } else if (!list.at(1).isString()) { + LOG(("Payments Error: " + "Not a string received in buy_callback result.")); + return; + } + + auto error = QJsonParseError(); + const auto document = QJsonDocument::fromJson( + list.at(1).toString().toUtf8(), + &error); + if (error.error != QJsonParseError::NoError) { + LOG(("Payments Error: " + "Failed to parse buy_callback arguments, error: %1." + ).arg(error.errorString())); + return; + } else if (!document.isObject()) { + LOG(("Payments Error: " + "Not an object decoded in buy_callback result.")); + return; + } + const auto root = document.object(); + const auto title = root.value("title").toString(); + const auto credentials = root.value("credentials"); + if (!credentials.isObject()) { + LOG(("Payments Error: " + "Not an object received in payment credentials.")); + return; + } + const auto serializedCredentials = QJsonDocument( + credentials.toObject() + ).toJson(QJsonDocument::Compact); + + _form->send(serializedCredentials); +} + +bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { + if (Core::TryConvertUrlToLocal(uri) == uri) { + return true; + } + panelCloseSure(); + App::wnd()->activate(); + return false; +} + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h new file mode 100644 index 0000000000..1eff37db96 --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -0,0 +1,69 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "payments/ui/payments_panel_delegate.h" +#include "base/weak_ptr.h" + +class HistoryItem; + +namespace Main { +class Session; +} // namespace Main + +namespace Payments::Ui { +class Panel; +class WebviewWindow; +} // namespace Payments::Ui + +namespace Payments { + +class Form; +struct FormUpdate; +struct FormError; +struct SendError; + +class CheckoutProcess final + : public base::has_weak_ptr + , private Ui::PanelDelegate { + struct PrivateTag {}; + +public: + static void Start(not_null item); + + CheckoutProcess( + not_null session, + FullMsgId itemId, + PrivateTag); + ~CheckoutProcess(); + + void requestActivate(); + +private: + [[nodiscard]] not_null panelDelegate(); + + void handleFormUpdate(const FormUpdate &update); + void handleFormError(const FormError &error); + void handleSendError(const SendError &error); + + void panelRequestClose() override; + void panelCloseSure() override; + void panelSubmit() override; + void panelWebviewMessage(const QJsonDocument &message) override; + bool panelWebviewNavigationAttempt(const QString &uri) override; + + const not_null _session; + const std::unique_ptr _form; + const std::unique_ptr _panel; + std::unique_ptr _webviewWindow; + + rpl::lifetime _lifetime; + +}; + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp new file mode 100644 index 0000000000..397a5058f1 --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -0,0 +1,154 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/payments_form.h" + +#include "main/main_session.h" +#include "data/data_session.h" +#include "apiwrap.h" + +namespace Payments { +namespace { + +[[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) { + return address.match([](const MTPDpostAddress &data) { + return Ui::Address{ + .address1 = qs(data.vstreet_line1()), + .address2 = qs(data.vstreet_line2()), + .city = qs(data.vcity()), + .state = qs(data.vstate()), + .countryIso2 = qs(data.vcountry_iso2()), + .postCode = qs(data.vpost_code()), + }; + }); +} + +} // namespace + +Form::Form(not_null session, FullMsgId itemId) +: _session(session) +, _msgId(itemId.msg) { + requestForm(); +} + +void Form::requestForm() { + _session->api().request(MTPpayments_GetPaymentForm( + MTP_int(_msgId) + )).done([=](const MTPpayments_PaymentForm &result) { + result.match([&](const auto &data) { + processForm(data); + }); + }).fail([=](const MTP::Error &error) { + _updates.fire({ FormError{ error.type() } }); + }).send(); +} + +void Form::processForm(const MTPDpayments_paymentForm &data) { + _session->data().processUsers(data.vusers()); + + data.vinvoice().match([&](const auto &data) { + processInvoice(data); + }); + processDetails(data); + if (const auto info = data.vsaved_info()) { + info->match([&](const auto &data) { + processSavedInformation(data); + }); + } + if (const auto credentials = data.vsaved_credentials()) { + credentials->match([&](const auto &data) { + processSavedCredentials(data); + }); + } + + _updates.fire({ FormReady{} }); +} + +void Form::processInvoice(const MTPDinvoice &data) { + auto &&prices = ranges::views::all( + data.vprices().v + ) | ranges::views::transform([](const MTPLabeledPrice &price) { + return price.match([&](const MTPDlabeledPrice &data) { + return Ui::LabeledPrice{ + .label = qs(data.vlabel()), + .price = data.vamount().v, + }; + }); + }); + _invoice = Ui::Invoice{ + .prices = prices | ranges::to_vector, + .currency = qs(data.vcurrency()), + + .isNameRequested = data.is_name_requested(), + .isPhoneRequested = data.is_phone_requested(), + .isEmailRequested = data.is_email_requested(), + .isShippingAddressRequested = data.is_shipping_address_requested(), + .isFlexible = data.is_flexible(), + .isTest = data.is_test(), + + .phoneSentToProvider = data.is_phone_to_provider(), + .emailSentToProvider = data.is_email_to_provider(), + }; +} + +void Form::processDetails(const MTPDpayments_paymentForm &data) { + _session->data().processUsers(data.vusers()); + const auto nativeParams = data.vnative_params(); + auto nativeParamsJson = nativeParams + ? nativeParams->match( + [&](const MTPDdataJSON &data) { return data.vdata().v; }) + : QByteArray(); + _details = FormDetails{ + .url = qs(data.vurl()), + .nativeProvider = qs(data.vnative_provider().value_or_empty()), + .nativeParamsJson = std::move(nativeParamsJson), + .botId = data.vbot_id().v, + .providerId = data.vprovider_id().v, + .canSaveCredentials = data.is_can_save_credentials(), + .passwordMissing = data.is_password_missing(), + }; +} + +void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { + const auto address = data.vshipping_address(); + _savedInformation = Ui::SavedInformation{ + .name = qs(data.vname().value_or_empty()), + .phone = qs(data.vphone().value_or_empty()), + .email = qs(data.vemail().value_or_empty()), + .shippingAddress = address ? ParseAddress(*address) : Ui::Address(), + }; +} + +void Form::processSavedCredentials( + const MTPDpaymentSavedCredentialsCard &data) { + _savedCredentials = Ui::SavedCredentials{ + .id = qs(data.vid()), + .title = qs(data.vtitle()), + }; +} + +void Form::send(const QByteArray &serializedCredentials) { + _session->api().request(MTPpayments_SendPaymentForm( + MTP_flags(0), + MTP_int(_msgId), + MTPstring(), // requested_info_id + MTPstring(), // shipping_option_id, + MTP_inputPaymentCredentials( + MTP_flags(0), + MTP_dataJSON(MTP_bytes(serializedCredentials))) + )).done([=](const MTPpayments_PaymentResult &result) { + result.match([&](const MTPDpayments_paymentResult &data) { + _updates.fire({ PaymentFinished{ data.vupdates() } }); + }, [&](const MTPDpayments_paymentVerificationNeeded &data) { + _updates.fire({ VerificationNeeded{ qs(data.vurl()) } }); + }); + }).fail([=](const MTP::Error &error) { + _updates.fire({ SendError{ error.type() } }); + }).send(); +} + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h new file mode 100644 index 0000000000..92288605ce --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -0,0 +1,106 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "payments/ui/payments_panel_data.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Payments { + +struct FormDetails { + QString url; + QString nativeProvider; + QByteArray nativeParamsJson; + UserId botId = 0; + UserId providerId = 0; + bool canSaveCredentials = false; + bool passwordMissing = false; + + [[nodiscard]] bool valid() const { + return !url.isEmpty(); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + +struct FormReady {}; + +struct FormError { + QString type; +}; + +struct SendError { + QString type; +}; + +struct VerificationNeeded { + QString url; +}; + +struct PaymentFinished { + MTPUpdates updates; +}; + +struct FormUpdate { + std::variant< + FormReady, + FormError, + SendError, + VerificationNeeded, + PaymentFinished> data; +}; + +class Form final { +public: + Form(not_null session, FullMsgId itemId); + + [[nodiscard]] const Ui::Invoice &invoice() const { + return _invoice; + } + [[nodiscard]] const FormDetails &details() const { + return _details; + } + [[nodiscard]] const Ui::SavedInformation &savedInformation() const { + return _savedInformation; + } + [[nodiscard]] const Ui::SavedCredentials &savedCredentials() const { + return _savedCredentials; + } + + [[nodiscard]] rpl::producer updates() const { + return _updates.events(); + } + + void send(const QByteArray &serializedCredentials); + +private: + void requestForm(); + void processForm(const MTPDpayments_paymentForm &data); + void processInvoice(const MTPDinvoice &data); + void processDetails(const MTPDpayments_paymentForm &data); + void processSavedInformation(const MTPDpaymentRequestedInfo &data); + void processSavedCredentials( + const MTPDpaymentSavedCredentialsCard &data); + + const not_null _session; + MsgId _msgId = 0; + + Ui::Invoice _invoice; + FormDetails _details; + Ui::SavedInformation _savedInformation; + Ui::SavedCredentials _savedCredentials; + + rpl::event_stream _updates; + +}; + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style new file mode 100644 index 0000000000..6e99489ebd --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -0,0 +1,10 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +using "ui/basic.style"; + +using "passport/passport.style"; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp new file mode 100644 index 0000000000..935aa7cdd7 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -0,0 +1,87 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/ui/payments_form_summary.h" + +#include "payments/ui/payments_panel_delegate.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/fade_wrap.h" +#include "lang/lang_keys.h" +#include "styles/style_payments.h" +#include "styles/style_passport.h" + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +FormSummary::FormSummary( + QWidget *parent, + const Invoice &invoice, + not_null delegate) +: _delegate(delegate) +, _scroll(this, st::passportPanelScroll) +, _topShadow(this) +, _bottomShadow(this) +, _submit( + this, + tr::lng_payments_pay_amount(lt_amount, rpl::single(QString("much"))), + st::passportPanelAuthorize) { + setupControls(); +} + +void FormSummary::setupControls() { + const auto inner = setupContent(); + + _submit->addClickHandler([=] { + _delegate->panelSubmit(); + }); + + using namespace rpl::mappers; + + _topShadow->toggleOn( + _scroll->scrollTopValue() | rpl::map(_1 > 0)); + _bottomShadow->toggleOn(rpl::combine( + _scroll->scrollTopValue(), + _scroll->heightValue(), + inner->heightValue(), + _1 + _2 < _3)); +} + +not_null FormSummary::setupContent() { + const auto inner = _scroll->setOwnedWidget( + object_ptr(this)); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + inner->resizeToWidth(width); + }, inner->lifetime()); + + return inner; +} + +void FormSummary::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void FormSummary::updateControlsGeometry() { + const auto submitTop = height() - _submit->height(); + _scroll->setGeometry(0, 0, width(), submitTop); + _topShadow->resizeToWidth(width()); + _topShadow->moveToLeft(0, 0); + _bottomShadow->resizeToWidth(width()); + _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); + _submit->setFullWidth(width()); + _submit->moveToLeft(0, submitTop); + + _scroll->updateBars(); +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h new file mode 100644 index 0000000000..39ca9e7f06 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -0,0 +1,48 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" +#include "payments/ui/payments_panel_data.h" +#include "base/object_ptr.h" + +namespace Ui { +class ScrollArea; +class FadeShadow; +class RoundButton; +} // namespace Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +class FormSummary final : public RpWidget { +public: + FormSummary( + QWidget *parent, + const Invoice &invoice, + not_null delegate); + +private: + void resizeEvent(QResizeEvent *e) override; + + void setupControls(); + [[nodiscard]] not_null setupContent(); + void updateControlsGeometry(); + + const not_null _delegate; + object_ptr _scroll; + object_ptr _topShadow; + object_ptr _bottomShadow; + object_ptr _submit; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp new file mode 100644 index 0000000000..7c5ab32fd6 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -0,0 +1,47 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/ui/payments_panel.h" + +#include "payments/ui/payments_form_summary.h" +#include "payments/ui/payments_panel_delegate.h" +#include "ui/widgets/separate_panel.h" +#include "lang/lang_keys.h" +#include "styles/style_payments.h" +#include "styles/style_passport.h" + +namespace Payments::Ui { + +Panel::Panel(not_null delegate) +: _delegate(delegate) +, _widget(std::make_unique()) { + _widget->setTitle(tr::lng_payments_checkout_title()); + _widget->setInnerSize(st::passportPanelSize); + + _widget->closeRequests( + ) | rpl::start_with_next([=] { + _delegate->panelRequestClose(); + }, _widget->lifetime()); + + _widget->closeEvents( + ) | rpl::start_with_next([=] { + _delegate->panelCloseSure(); + }, _widget->lifetime()); +} + +Panel::~Panel() = default; + +void Panel::requestActivate() { + _widget->showAndActivate(); +} + +void Panel::showForm(const Invoice &invoice) { + _widget->showInner( + base::make_unique_q(_widget.get(), invoice, _delegate)); +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h new file mode 100644 index 0000000000..0aa68c456f --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -0,0 +1,36 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui { +class SeparatePanel; +} // namespace Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; +struct Invoice; + +class Panel final { +public: + explicit Panel(not_null delegate); + ~Panel(); + + void requestActivate(); + + void showForm(const Invoice &invoice); + +private: + const not_null _delegate; + std::unique_ptr _widget; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h new file mode 100644 index 0000000000..7351363500 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -0,0 +1,86 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Payments::Ui { + +struct LabeledPrice { + QString label; + uint64 price = 0; +}; + +struct Invoice { + std::vector prices; + QString currency; + + bool isNameRequested = false; + bool isPhoneRequested = false; + bool isEmailRequested = false; + bool isShippingAddressRequested = false; + bool isFlexible = false; + bool isTest = false; + + bool phoneSentToProvider = false; + bool emailSentToProvider = false; + + [[nodiscard]] bool valid() const { + return !currency.isEmpty() && !prices.empty(); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + +struct Address { + QString address1; + QString address2; + QString city; + QString state; + QString countryIso2; + QString postCode; + + [[nodiscard]] bool valid() const { + return !address1.isEmpty() + && !city.isEmpty() + && !countryIso2.isEmpty(); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + +struct SavedInformation { + QString name; + QString phone; + QString email; + Address shippingAddress; + + [[nodiscard]] bool empty() const { + return name.isEmpty() + && phone.isEmpty() + && email.isEmpty() + && !shippingAddress; + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } +}; + +struct SavedCredentials { + QString id; + QString title; + + [[nodiscard]] bool valid() const { + return !id.isEmpty(); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h new file mode 100644 index 0000000000..49cc5b988a --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -0,0 +1,24 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class QJsonDocument; +class QString; + +namespace Payments::Ui { + +class PanelDelegate { +public: + virtual void panelRequestClose() = 0; + virtual void panelCloseSure() = 0; + virtual void panelSubmit() = 0; + virtual void panelWebviewMessage(const QJsonDocument &message) = 0; + virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_webview.cpp b/Telegram/SourceFiles/payments/ui/payments_webview.cpp new file mode 100644 index 0000000000..02ff0d1938 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_webview.cpp @@ -0,0 +1,94 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "payments/ui/payments_webview.h" + +#include "payments/ui/payments_panel_delegate.h" +#include "webview/webview_embed.h" +#include "webview/webview_interface.h" +#include "ui/widgets/window.h" +#include "ui/toast/toast.h" +#include "lang/lang_keys.h" + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +WebviewWindow::WebviewWindow( + const QString &url, + not_null delegate) { + if (!url.startsWith("https://", Qt::CaseInsensitive)) { + return; + } + + const auto window = &_window; + + window->setGeometry({ + style::ConvertScale(100), + style::ConvertScale(100), + style::ConvertScale(640), + style::ConvertScale(480) + }); + window->show(); + + window->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Close) { + delegate->panelCloseSure(); + } + }, window->lifetime()); + + const auto body = window->body(); + body->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(body).fillRect(clip, st::windowBg); + }, body->lifetime()); + + _webview = Ui::CreateChild( + window, + window); + if (!_webview->widget()) { + return; + } + + body->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + _webview->widget()->setGeometry(geometry); + }, body->lifetime()); + + _webview->setMessageHandler([=](const QJsonDocument &message) { + delegate->panelWebviewMessage(message); + }); + + _webview->setNavigationHandler([=](const QString &uri) { + return delegate->panelWebviewNavigationAttempt(uri); + }); + + _webview->init(R"( +window.TelegramWebviewProxy = { +postEvent: function(eventType, eventData) { + if (window.external && window.external.invoke) { + window.external.invoke(JSON.stringify([eventType, eventData])); + } +} +};)"); + + navigate(url); +} + +[[nodiscard]] bool WebviewWindow::shown() const { + return _webview && _webview->widget(); +} + +void WebviewWindow::navigate(const QString &url) { + if (shown()) { + _webview->navigate(url); + } +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_webview.h b/Telegram/SourceFiles/payments/ui/payments_webview.h new file mode 100644 index 0000000000..738c779941 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_webview.h @@ -0,0 +1,37 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/widgets/window.h" + +namespace Webview { +class Window; +} // namespace Webview + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +class WebviewWindow final { +public: + WebviewWindow( + const QString &url, + not_null delegate); + + [[nodiscard]] bool shown() const; + void navigate(const QString &url); + +private: + Ui::Window _window; + Webview::Window *_webview = nullptr; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp index 281bc83874..84f8097b89 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/widgets/separate_panel.h" -#include "window/main_window.h" #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -17,14 +16,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/tooltip.h" #include "ui/platform/ui_platform_utility.h" #include "ui/layers/layer_widget.h" -#include "window/themes/window_theme.h" -#include "core/application.h" -#include "app.h" #include "styles/style_widgets.h" #include "styles/style_info.h" #include "styles/style_calls.h" +#include "logs.h" // #TODO logs #include +#include #include #include @@ -35,7 +33,7 @@ SeparatePanel::SeparatePanel() , _back(this, object_ptr(this, st::separatePanelBack)) , _body(this) { setMouseTracking(true); - setWindowIcon(Window::CreateIcon()); + setWindowIcon(QGuiApplication::windowIcon()); initControls(); initLayout(); } @@ -155,13 +153,11 @@ void SeparatePanel::initLayout() { setAttribute(Qt::WA_TranslucentBackground, true); createBorderImage(); - subscribe(Window::Theme::Background(), [=]( - const Window::Theme::BackgroundUpdate &update) { - if (update.paletteChanged()) { - createBorderImage(); - Ui::ForceFullRepaint(this); - } - }); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + createBorderImage(); + Ui::ForceFullRepaint(this); + }, lifetime()); Ui::Platform::InitOnTopPanel(this); } @@ -170,10 +166,10 @@ void SeparatePanel::createBorderImage() { const auto shadowPadding = st::callShadow.extend; const auto cacheSize = st::separatePanelBorderCacheSize; auto cache = QImage( - cacheSize * cIntRetinaFactor(), - cacheSize * cIntRetinaFactor(), + cacheSize * style::DevicePixelRatio(), + cacheSize * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); - cache.setDevicePixelRatio(cRetinaFactor()); + cache.setDevicePixelRatio(style::DevicePixelRatio()); cache.fill(Qt::transparent); { Painter p(&cache); @@ -189,7 +185,7 @@ void SeparatePanel::createBorderImage() { st::callRadius, st::callRadius); } - _borderParts = App::pixmapFromImageInPlace(std::move(cache)); + _borderParts = Ui::PixmapFromImage(std::move(cache)); } void SeparatePanel::toggleOpacityAnimation(bool visible) { @@ -346,7 +342,12 @@ void SeparatePanel::setInnerSize(QSize size) { } void SeparatePanel::initGeometry(QSize size) { - const auto center = Core::App().getPointForCallPanelCenter(); + const auto active = QApplication::activeWindow(); + const auto center = !active + ? QGuiApplication::primaryScreen()->geometry().center() + : (active->isVisible() && active->isActiveWindow()) + ? active->geometry().center() + : active->windowHandle()->screen()->geometry().center(); _useTransparency = Ui::Platform::TranslucentWindowsSupported(center); _padding = _useTransparency ? st::callShadow.extend @@ -427,7 +428,7 @@ void SeparatePanel::paintEvent(QPaintEvent *e) { } void SeparatePanel::paintShadowBorder(Painter &p) const { - const auto factor = cIntRetinaFactor(); + const auto factor = style::DevicePixelRatio(); const auto size = st::separatePanelBorderCacheSize; const auto part1 = size / 3; const auto part2 = size - part1; diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.h b/Telegram/SourceFiles/ui/widgets/separate_panel.h index 92608fac1c..95bfd5871f 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.h +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.h @@ -22,7 +22,7 @@ class FadeWrapScaled; namespace Ui { -class SeparatePanel : public Ui::RpWidget, private base::Subscriber { +class SeparatePanel final : public Ui::RpWidget { public: SeparatePanel(); diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index eded7cc540..6549849146 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit eded7cc540123eaf26361958b9a61c65cb2f7cfc +Subproject commit 65498491465fa64ffdf96a2b7cdeb67bfb697d5b diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 6c22d51663..57338bf699 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -24,6 +24,7 @@ set(style_files intro/intro.style media/player/media_player.style passport/passport.style + payments/ui/payments.style profile/profile.style settings/settings.style media/view/media_view.style @@ -60,6 +61,15 @@ PRIVATE media/clip/media_clip_reader.cpp media/clip/media_clip_reader.h + payments/ui/payments_form_summary.cpp + payments/ui/payments_form_summary.h + payments/ui/payments_panel.cpp + payments/ui/payments_panel.h + payments/ui/payments_panel_data.h + payments/ui/payments_panel_delegate.h + payments/ui/payments_webview.cpp + payments/ui/payments_webview.h + platform/mac/file_bookmark_mac.h platform/mac/file_bookmark_mac.mm platform/platform_file_bookmark.h @@ -114,6 +124,8 @@ PRIVATE ui/text/text_options.h ui/toasts/common_toasts.cpp ui/toasts/common_toasts.h + ui/widgets/separate_panel.cpp + ui/widgets/separate_panel.h ui/cached_round_corners.cpp ui/cached_round_corners.h ui/grouped_layout.cpp @@ -131,6 +143,8 @@ target_link_libraries(td_ui PUBLIC tdesktop::td_lang desktop-app::lib_ui - desktop-app::lib_ffmpeg desktop-app::lib_lottie +PRIVATE + desktop-app::lib_ffmpeg + desktop-app::lib_webview ) diff --git a/Telegram/lib_base b/Telegram/lib_base index 8f0c0164cd..7a5fd82692 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 8f0c0164cdce6bcbc7bcfe531963e2a552f6290d +Subproject commit 7a5fd82692d2fb5df9b48c08c354f4400157a999 diff --git a/Telegram/lib_tl b/Telegram/lib_tl index 404c83d77e..45faed44e7 160000 --- a/Telegram/lib_tl +++ b/Telegram/lib_tl @@ -1 +1 @@ -Subproject commit 404c83d77e5edb8a39f8e9f56a6340960fe5070e +Subproject commit 45faed44e7f4d11fec79b7a70e4a35dc91ef3fdb diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 99089134e3..8686905ee4 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 99089134e34c19e4c6fdb25569ade0d6f081bdb1 +Subproject commit 8686905ee40eb8dbe171024e04e41a32069c8add diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index f95214cbe4..5270a1dbbd 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit f95214cbe4b0a31ac989e0aceb8cc4f63c1322e6 +Subproject commit 5270a1dbbdbee643e187e175f798595b4bc49996 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 7491d16023..49887261a5 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 7491d160231a18dec6aec1f3c1e1575382d10745 +Subproject commit 49887261a55665f6e195049bcc22b6495a44cc36