From 22da48d231aa291ebc3ab33c84b983227c6b7516 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Feb 2021 15:23:25 +0400 Subject: [PATCH 001/127] Add webview / lib_webview submodules. --- .gitmodules | 6 ++++++ Telegram/ThirdParty/webview | 1 + Telegram/lib_webview | 1 + 3 files changed, 8 insertions(+) create mode 160000 Telegram/ThirdParty/webview create mode 160000 Telegram/lib_webview diff --git a/.gitmodules b/.gitmodules index 17a1be422..a8bed44c9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -88,3 +88,9 @@ [submodule "Telegram/ThirdParty/tgcalls"] path = Telegram/ThirdParty/tgcalls url = https://github.com/TelegramMessenger/tgcalls.git +[submodule "Telegram/ThirdParty/webview"] + path = Telegram/ThirdParty/webview + url = https://github.com/desktop-app/webview.git +[submodule "Telegram/lib_webview"] + path = Telegram/lib_webview + url = https://github.com/desktop-app/lib_webview.git diff --git a/Telegram/ThirdParty/webview b/Telegram/ThirdParty/webview new file mode 160000 index 000000000..b49b40100 --- /dev/null +++ b/Telegram/ThirdParty/webview @@ -0,0 +1 @@ +Subproject commit b49b40100e9abca5cf3cb67874e3e31ae9241573 diff --git a/Telegram/lib_webview b/Telegram/lib_webview new file mode 160000 index 000000000..5314cb46c --- /dev/null +++ b/Telegram/lib_webview @@ -0,0 +1 @@ +Subproject commit 5314cb46cdcdae6db049df18ca38dfd2d98ee4bf From fd85efa9ba34285740f7ad1079ae4a234d15d455 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Feb 2021 18:36:54 +0400 Subject: [PATCH 002/127] Link Telegram with lib_webview. --- .gitmodules | 3 --- Telegram/CMakeLists.txt | 3 +++ Telegram/ThirdParty/webview | 1 - Telegram/lib_webview | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 160000 Telegram/ThirdParty/webview diff --git a/.gitmodules b/.gitmodules index a8bed44c9..f982f14b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -88,9 +88,6 @@ [submodule "Telegram/ThirdParty/tgcalls"] path = Telegram/ThirdParty/tgcalls url = https://github.com/TelegramMessenger/tgcalls.git -[submodule "Telegram/ThirdParty/webview"] - path = Telegram/ThirdParty/webview - url = https://github.com/desktop-app/webview.git [submodule "Telegram/lib_webview"] path = Telegram/lib_webview url = https://github.com/desktop-app/lib_webview.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8a999813d..eaaf8d0a8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -19,6 +19,7 @@ add_subdirectory(lib_storage) add_subdirectory(lib_lottie) add_subdirectory(lib_qr) add_subdirectory(lib_webrtc) +add_subdirectory(lib_webview) add_subdirectory(codegen) get_filename_component(src_loc SourceFiles REALPATH) @@ -55,6 +56,8 @@ PRIVATE desktop-app::lib_storage desktop-app::lib_lottie desktop-app::lib_qr + desktop-app::lib_webview + desktop-app::lib_webview_wrap desktop-app::lib_ffmpeg desktop-app::external_lz4 desktop-app::external_rlottie diff --git a/Telegram/ThirdParty/webview b/Telegram/ThirdParty/webview deleted file mode 160000 index b49b40100..000000000 --- a/Telegram/ThirdParty/webview +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b49b40100e9abca5cf3cb67874e3e31ae9241573 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 5314cb46c..64c0b1e57 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 5314cb46cdcdae6db049df18ca38dfd2d98ee4bf +Subproject commit 64c0b1e57e2fc6383b9af999ed3c2db220562492 From 7c979144fcf04327b8171a59a0e4f6ad4947af17 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Feb 2021 18:38:16 +0400 Subject: [PATCH 003/127] Proof-Of-Concept simplest invoice payment. --- Telegram/SourceFiles/facades.cpp | 125 ++++++++++++++++++++++++++++++- Telegram/lib_webview | 2 +- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index c35948c2e..6bb2e2940 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -34,6 +34,124 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "styles/style_chat.h" +#include "webview/webview_embed.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({ 100, 100, 1280, 960 }); + window->show(); + + const auto body = window->body(); + const auto webview = new Webview::Window(window); + body->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + webview->widget()->setGeometry(geometry); + }, body->lifetime()); + + webview->bind("buy_callback", [=](const QByteArray &result) { + auto error = QJsonParseError{ 0, QJsonParseError::NoError }; + const auto typeAndArguments = QJsonDocument::fromJson( + result, + &error); + if (error.error != QJsonParseError::NoError) { + LOG(("Payments Error: " + "Failed to parse buy_callback result, error: %1." + ).arg(error.errorString())); + return; + } else if (!typeAndArguments.isArray()) { + LOG(("API Error: " + "Not an array received in buy_callback arguments.")); + return; + } + const auto list = typeAndArguments.array(); + if (list.at(0).toString() != "payment_form_submit") { + return; + } else if (!list.at(1).isString()) { + LOG(("API Error: " + "Not a string received in buy_callback result.")); + return; + } + + 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(("API 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(("API 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) { + delete window; + App::wnd()->activate(); + result.match([&](const MTPDpayments_paymentResult &data) { + session->api().applyUpdates(data.vupdates()); + }, [&](const MTPDpayments_paymentVerificationNeeded &data) { + Ui::Toast::Show("payments.paymentVerificationNeeded"); + }); + }).fail([=](const RPCError &error) { + delete window; + App::wnd()->activate(); + Ui::Toast::Show("payments.sendPaymentForm: " + error.type()); + }).send(); + }); + + webview->init("(function(){" + "window.TelegramWebviewProxy = {" + "postEvent: function(eventType, eventData) {" + "if (window.buy_callback) {" + "window.buy_callback(eventType, eventData);" + "}" + "}" + "};" + "}());"); + + const auto &data = result.c_payments_paymentForm(); + webview->navigate(qs(data.vurl().v)); + }).fail([=](const RPCError &error) { + App::wnd()->activate(); + Ui::Toast::Show("payments.getPaymentForm: " + error.type()); + }).send(); +} + +} // namespace Api + namespace { [[nodiscard]] MainWidget *CheckMainWidget(not_null session) { @@ -121,7 +239,8 @@ void activateBotCommand( } break; case ButtonType::Buy: { - Ui::show(Box(tr::lng_payments_not_supported(tr::now))); + Api::GetPaymentForm(msg); + //Ui::show(Box(tr::lng_payments_not_supported(tr::now))); } break; case ButtonType::Url: { @@ -260,7 +379,7 @@ void showChatsList(not_null session) { if (const auto m = CheckMainWidget(session)) { m->ui_showPeerHistory( 0, - Window::SectionShow::Way::ClearStack, + ::Window::SectionShow::Way::ClearStack, 0); } } @@ -277,7 +396,7 @@ void showPeerHistory(not_null peer, MsgId msgId) { if (const auto m = CheckMainWidget(&peer->session())) { m->ui_showPeerHistory( peer->id, - Window::SectionShow::Way::ClearStack, + ::Window::SectionShow::Way::ClearStack, msgId); } } diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 64c0b1e57..34fed9f8a 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 64c0b1e57e2fc6383b9af999ed3c2db220562492 +Subproject commit 34fed9f8a9a64c03bdc9682fbacc216607cef6cf From b323e5ffcff8a55cac9e3a82cc6022bd3b22e081 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Feb 2021 20:56:12 +0400 Subject: [PATCH 004/127] 3DSecure in Proof-Of-Concept payments. --- Telegram/SourceFiles/facades.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 6bb2e2940..33a316b54 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -54,8 +54,16 @@ void GetPaymentForm(not_null msg) { window->setGeometry({ 100, 100, 1280, 960 }); 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(); - const auto webview = new Webview::Window(window); + const auto webview = Ui::CreateChild( + window, + window); body->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { webview->widget()->setGeometry(geometry); @@ -118,12 +126,12 @@ void GetPaymentForm(not_null msg) { MTP_flags(0), MTP_dataJSON(MTP_bytes(serializedCredentials))) )).done([=](const MTPpayments_PaymentResult &result) { - delete window; - App::wnd()->activate(); result.match([&](const MTPDpayments_paymentResult &data) { + delete window; + App::wnd()->activate(); session->api().applyUpdates(data.vupdates()); }, [&](const MTPDpayments_paymentVerificationNeeded &data) { - Ui::Toast::Show("payments.paymentVerificationNeeded"); + webview->navigate(qs(data.vurl())); }); }).fail([=](const RPCError &error) { delete window; @@ -143,7 +151,7 @@ void GetPaymentForm(not_null msg) { "}());"); const auto &data = result.c_payments_paymentForm(); - webview->navigate(qs(data.vurl().v)); + webview->navigate(qs(data.vurl())); }).fail([=](const RPCError &error) { App::wnd()->activate(); Ui::Toast::Show("payments.getPaymentForm: " + error.type()); From 21228783da95d9ad857b425d806795b0d939f4fd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Feb 2021 21:19:08 +0400 Subject: [PATCH 005/127] Fix webview on macOS. --- Telegram/SourceFiles/facades.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 33a316b54..c51e35885 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -51,7 +51,12 @@ void GetPaymentForm(not_null msg) { MTP_int(msgId) )).done([=](const MTPpayments_PaymentForm &result) { const auto window = new Ui::Window(); - window->setGeometry({ 100, 100, 1280, 960 }); + 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) { From 35610da7508cb51e060cef764fabb0c88a85f1ce Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Mar 2021 15:47:08 +0400 Subject: [PATCH 006/127] Use lib_webview implementation on Windows. --- Telegram/SourceFiles/facades.cpp | 68 ++++++++++++++++++++------------ docs/building-msvc-x64.md | 2 + docs/building-msvc.md | 2 + 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index c51e35885..bb1984171 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -35,6 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 @@ -66,38 +68,42 @@ void GetPaymentForm(not_null msg) { }, 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->bind("buy_callback", [=](const QByteArray &result) { - auto error = QJsonParseError{ 0, QJsonParseError::NoError }; - const auto typeAndArguments = QJsonDocument::fromJson( - result, - &error); - if (error.error != QJsonParseError::NoError) { + webview->setMessageHandler([=](const QJsonDocument &message) { + if (!message.isArray()) { LOG(("Payments Error: " - "Failed to parse buy_callback result, error: %1." - ).arg(error.errorString())); - return; - } else if (!typeAndArguments.isArray()) { - LOG(("API Error: " "Not an array received in buy_callback arguments.")); return; } - const auto list = typeAndArguments.array(); + const auto list = message.array(); if (list.at(0).toString() != "payment_form_submit") { return; } else if (!list.at(1).isString()) { - LOG(("API Error: " + 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); @@ -107,7 +113,7 @@ void GetPaymentForm(not_null msg) { ).arg(error.errorString())); return; } else if (!document.isObject()) { - LOG(("API Error: " + LOG(("Payments Error: " "Not an object decoded in buy_callback result.")); return; } @@ -115,7 +121,7 @@ void GetPaymentForm(not_null msg) { const auto title = root.value("title").toString(); const auto credentials = root.value("credentials"); if (!credentials.isObject()) { - LOG(("API Error: " + LOG(("Payments Error: " "Not an object received in payment credentials.")); return; } @@ -145,15 +151,21 @@ void GetPaymentForm(not_null msg) { }).send(); }); - webview->init("(function(){" - "window.TelegramWebviewProxy = {" - "postEvent: function(eventType, eventData) {" - "if (window.buy_callback) {" - "window.buy_callback(eventType, eventData);" - "}" - "}" - "};" - "}());"); + webview->setNavigationHandler([=](const QString &uri) { + if (Core::TryConvertUrlToLocal(uri) != uri) { + window->deleteLater(); + App::wnd()->activate(); + } + }); + + 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())); @@ -252,8 +264,12 @@ void activateBotCommand( } break; case ButtonType::Buy: { - Api::GetPaymentForm(msg); - //Ui::show(Box(tr::lng_payments_not_supported(tr::now))); + if (Webview::Supported()) { + Api::GetPaymentForm(msg); + } else { + Ui::show(Box( + tr::lng_payments_not_supported(tr::now))); + } } break; case ButtonType::Url: { diff --git a/docs/building-msvc-x64.md b/docs/building-msvc-x64.md index 5750c29fa..f5e73e1a6 100644 --- a/docs/building-msvc-x64.md +++ b/docs/building-msvc-x64.md @@ -27,6 +27,7 @@ You will require **api_id** and **api_hash** to access the Telegram API servers. * Download **CMake** installer from [https://cmake.org/download/](https://cmake.org/download/) and install to ***BuildPath*\\ThirdParty\\cmake** * Download **Ninja** executable from [https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-win.zip](https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-win.zip) and unpack to ***BuildPath*\\ThirdParty\\Ninja** * Download **Git** installer from [https://git-scm.com/download/win](https://git-scm.com/download/win) and install it. +* Download **NuGet** executable from [https://dist.nuget.org/win-x86-commandline/latest/nuget.exe](https://www.nuget.org/downloads) and put to ***BuildPath*\\ThirdParty\\NuGet** Open **x64 Native Tools Command Prompt for VS 2019.bat**, go to ***BuildPath*** and run @@ -49,6 +50,7 @@ Add **GYP** and **Ninja** to your PATH: * Press **Edit** * Add ***BuildPath*\\ThirdParty\\gyp** value * Add ***BuildPath*\\ThirdParty\\Ninja** value +* Add ***BuildPath*\\ThirdParty\\NuGet** value ## Clone source code and prepare libraries diff --git a/docs/building-msvc.md b/docs/building-msvc.md index 243a385fb..9499a5a4e 100644 --- a/docs/building-msvc.md +++ b/docs/building-msvc.md @@ -27,6 +27,7 @@ You will require **api_id** and **api_hash** to access the Telegram API servers. * Download **CMake** installer from [https://cmake.org/download/](https://cmake.org/download/) and install to ***BuildPath*\\ThirdParty\\cmake** * Download **Ninja** executable from [https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-win.zip](https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-win.zip) and unpack to ***BuildPath*\\ThirdParty\\Ninja** * Download **Git** installer from [https://git-scm.com/download/win](https://git-scm.com/download/win) and install it. +* Download **NuGet** executable from [https://dist.nuget.org/win-x86-commandline/latest/nuget.exe](https://www.nuget.org/downloads) and put to ***BuildPath*\\ThirdParty\\NuGet** Open **x86 Native Tools Command Prompt for VS 2019.bat**, go to ***BuildPath*** and run @@ -49,6 +50,7 @@ Add **GYP** and **Ninja** to your PATH: * Press **Edit** * Add ***BuildPath*\\ThirdParty\\gyp** value * Add ***BuildPath*\\ThirdParty\\Ninja** value +* Add ***BuildPath*\\ThirdParty\\NuGet** value ## Clone source code and prepare libraries From f93527442d243e24769040bf0c0d18fa037079b4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Mar 2021 17:21:49 +0400 Subject: [PATCH 007/127] Use lib_webview implementation on macOS. --- Telegram/lib_webview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 34fed9f8a..631b2b851 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 34fed9f8a9a64c03bdc9682fbacc216607cef6cf +Subproject commit 631b2b851f2ad61bc4534db648f55289b38e1532 From 7b277aa7707f557043ddec24d50720968dca4a6c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Mar 2021 19:26:24 +0400 Subject: [PATCH 008/127] Start Linux support. --- Telegram/lib_webview | 2 +- cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 631b2b851..795acfbc3 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 631b2b851f2ad61bc4534db648f55289b38e1532 +Subproject commit 795acfbc30402b7503a3c315e7ec5b8be15e46d0 diff --git a/cmake b/cmake index bd9c097fe..52ccf5e17 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit bd9c097fea8b1f9deefa5b541a288e628ec732f5 +Subproject commit 52ccf5e17ab1bd7b352346c43706dc5e53bd19ca From c74e240d30e376c7716b8460d75d6ce04bd72983 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Mar 2021 21:44:22 +0400 Subject: [PATCH 009/127] Update lib_webview. --- Telegram/CMakeLists.txt | 8 ++++++-- Telegram/lib_webview | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index eaaf8d0a8..a5ac9cdf6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -57,7 +57,6 @@ PRIVATE desktop-app::lib_lottie desktop-app::lib_qr desktop-app::lib_webview - desktop-app::lib_webview_wrap desktop-app::lib_ffmpeg desktop-app::external_lz4 desktop-app::external_rlottie @@ -73,7 +72,12 @@ PRIVATE desktop-app::external_xxhash ) -if (LINUX) +if (WIN32) + target_link_libraries(Telegram + PRIVATE + desktop-app::lib_webview_winrt + ) +elseif (LINUX) target_link_libraries(Telegram PRIVATE desktop-app::external_glibmm diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 795acfbc3..3f004da92 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 795acfbc30402b7503a3c315e7ec5b8be15e46d0 +Subproject commit 3f004da92dd1368d8bad4bc3838941fef10af76d From 25bbde273900cf813cb3f14e1acbdc642f09cc46 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Mar 2021 21:54:25 +0400 Subject: [PATCH 010/127] Use navigation cancel in Webview. --- Telegram/SourceFiles/facades.cpp | 8 +++++--- Telegram/lib_webview | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index bb1984171..e285a9244 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -152,10 +152,12 @@ void GetPaymentForm(not_null msg) { }); webview->setNavigationHandler([=](const QString &uri) { - if (Core::TryConvertUrlToLocal(uri) != uri) { - window->deleteLater(); - App::wnd()->activate(); + if (Core::TryConvertUrlToLocal(uri) == uri) { + return true; } + window->deleteLater(); + App::wnd()->activate(); + return false; }); webview->init(R"( diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 3f004da92..7491d1602 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 3f004da92dd1368d8bad4bc3838941fef10af76d +Subproject commit 7491d160231a18dec6aec1f3c1e1575382d10745 From 4c707bff2922c2bf8d4188ae710fab23187cf5a4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 Mar 2021 16:34:34 +0400 Subject: [PATCH 011/127] Start proper payments processing. --- Telegram/CMakeLists.txt | 6 +- Telegram/Resources/langs/lang.strings | 19 ++ Telegram/SourceFiles/core/ui_integration.cpp | 4 + Telegram/SourceFiles/core/ui_integration.h | 2 + Telegram/SourceFiles/facades.cpp | 153 +---------- .../payments/payments_checkout_process.cpp | 240 ++++++++++++++++++ .../payments/payments_checkout_process.h | 69 +++++ .../SourceFiles/payments/payments_form.cpp | 154 +++++++++++ Telegram/SourceFiles/payments/payments_form.h | 106 ++++++++ .../SourceFiles/payments/ui/payments.style | 10 + .../payments/ui/payments_form_summary.cpp | 87 +++++++ .../payments/ui/payments_form_summary.h | 48 ++++ .../payments/ui/payments_panel.cpp | 47 ++++ .../SourceFiles/payments/ui/payments_panel.h | 36 +++ .../payments/ui/payments_panel_data.h | 86 +++++++ .../payments/ui/payments_panel_delegate.h | 24 ++ .../payments/ui/payments_webview.cpp | 94 +++++++ .../payments/ui/payments_webview.h | 37 +++ .../SourceFiles/ui/widgets/separate_panel.cpp | 37 +-- .../SourceFiles/ui/widgets/separate_panel.h | 2 +- Telegram/ThirdParty/tgcalls | 2 +- Telegram/cmake/td_ui.cmake | 16 +- Telegram/lib_base | 2 +- Telegram/lib_tl | 2 +- Telegram/lib_ui | 2 +- Telegram/lib_webrtc | 2 +- Telegram/lib_webview | 2 +- 27 files changed, 1110 insertions(+), 179 deletions(-) create mode 100644 Telegram/SourceFiles/payments/payments_checkout_process.cpp create mode 100644 Telegram/SourceFiles/payments/payments_checkout_process.h create mode 100644 Telegram/SourceFiles/payments/payments_form.cpp create mode 100644 Telegram/SourceFiles/payments/payments_form.h create mode 100644 Telegram/SourceFiles/payments/ui/payments.style create mode 100644 Telegram/SourceFiles/payments/ui/payments_form_summary.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_form_summary.h create mode 100644 Telegram/SourceFiles/payments/ui/payments_panel.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_panel.h create mode 100644 Telegram/SourceFiles/payments/ui/payments_panel_data.h create mode 100644 Telegram/SourceFiles/payments/ui/payments_panel_delegate.h create mode 100644 Telegram/SourceFiles/payments/ui/payments_webview.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_webview.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a5ac9cdf6..cb9eec67d 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 99b4ff084..0a2a9c6ae 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 fab5b1070..d2cf492b1 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 2aac9a0aa..5bdc337a4 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 e285a9244..31080b485 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 000000000..916150a20 --- /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 000000000..1eff37db9 --- /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 000000000..397a5058f --- /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 000000000..92288605c --- /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 000000000..6e99489eb --- /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 000000000..935aa7cdd --- /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 000000000..39ca9e7f0 --- /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 000000000..7c5ab32fd --- /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 000000000..0aa68c456 --- /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 000000000..735136350 --- /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 000000000..49cc5b988 --- /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 000000000..02ff0d193 --- /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 000000000..738c77994 --- /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 281bc8387..84f8097b8 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 92608fac1..95bfd5871 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 eded7cc54..654984914 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 6c22d5166..57338bf69 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 8f0c0164c..7a5fd8269 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 404c83d77..45faed44e 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 99089134e..8686905ee 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 f95214cbe..5270a1dbb 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 7491d1602..49887261a 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 7491d160231a18dec6aec1f3c1e1575382d10745 +Subproject commit 49887261a55665f6e195049bcc22b6495a44cc36 From 0d44736575fec51caaa3c938a47126094d08e922 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 Mar 2021 20:06:59 +0400 Subject: [PATCH 012/127] First full-featured version of payments, no design. --- Telegram/CMakeLists.txt | 6 - .../boxes/peers/edit_peer_info_box.cpp | 2 +- .../calls/calls_group_settings.cpp | 2 +- .../export/data/export_data_types.cpp | 2 +- .../export/data/export_data_types.h | 2 +- .../SourceFiles/history/history_service.cpp | 4 +- Telegram/SourceFiles/intro/intro_phone.cpp | 6 +- .../passport/passport_panel_controller.cpp | 38 +- .../passport/passport_panel_edit_contact.cpp | 2 +- .../passport/passport_panel_edit_document.cpp | 23 +- .../passport/passport_panel_edit_document.h | 14 +- .../passport/passport_panel_edit_scans.cpp | 2 +- .../passport/passport_panel_form.cpp | 140 +----- .../passport/passport_panel_form.h | 7 +- .../passport_details_row.cpp} | 129 ++--- .../passport_details_row.h} | 17 +- .../passport/ui/passport_form_row.cpp | 125 +++++ .../passport/ui/passport_form_row.h | 48 ++ .../payments/payments_checkout_process.cpp | 138 +++++- .../payments/payments_checkout_process.h | 24 + .../SourceFiles/payments/payments_form.cpp | 122 ++++- Telegram/SourceFiles/payments/payments_form.h | 26 +- .../SourceFiles/payments/ui/payments.style | 2 + .../payments/ui/payments_edit_information.cpp | 262 +++++++++++ .../payments/ui/payments_edit_information.h | 71 +++ .../payments/ui/payments_form_summary.cpp | 147 +++++- .../payments/ui/payments_form_summary.h | 8 + .../payments/ui/payments_panel.cpp | 60 ++- .../SourceFiles/payments/ui/payments_panel.h | 19 +- .../payments/ui/payments_panel_data.h | 44 +- .../payments/ui/payments_panel_delegate.h | 20 + .../SourceFiles/settings/settings_calls.cpp | 2 +- .../ui/boxes/country_select_box.cpp | 443 ++++++++++++++++++ .../SourceFiles/ui/boxes/country_select_box.h | 53 +++ .../{ => ui}/boxes/single_choice_box.cpp | 4 +- .../{ => ui}/boxes/single_choice_box.h | 0 Telegram/SourceFiles/ui/countryinput.cpp | 359 +------------- Telegram/SourceFiles/ui/countryinput.h | 99 +--- .../SourceFiles/ui/text/format_values.cpp | 2 +- Telegram/SourceFiles/ui/text/format_values.h | 2 +- .../SourceFiles/ui/widgets/multi_select.h | 2 + Telegram/cmake/td_ui.cmake | 14 + 42 files changed, 1756 insertions(+), 736 deletions(-) rename Telegram/SourceFiles/passport/{passport_panel_details_row.cpp => ui/passport_details_row.cpp} (90%) rename Telegram/SourceFiles/passport/{passport_panel_details_row.h => ui/passport_details_row.h} (83%) create mode 100644 Telegram/SourceFiles/passport/ui/passport_form_row.cpp create mode 100644 Telegram/SourceFiles/passport/ui/passport_form_row.h create mode 100644 Telegram/SourceFiles/payments/ui/payments_edit_information.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_edit_information.h create mode 100644 Telegram/SourceFiles/ui/boxes/country_select_box.cpp create mode 100644 Telegram/SourceFiles/ui/boxes/country_select_box.h rename Telegram/SourceFiles/{ => ui}/boxes/single_choice_box.cpp (94%) rename Telegram/SourceFiles/{ => ui}/boxes/single_choice_box.h (100%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cb9eec67d..ec5aa69c8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -256,8 +256,6 @@ PRIVATE boxes/sessions_box.h boxes/share_box.cpp boxes/share_box.h - boxes/single_choice_box.cpp - boxes/single_choice_box.h boxes/sticker_set_box.cpp boxes/sticker_set_box.h boxes/stickers_box.cpp @@ -390,8 +388,6 @@ PRIVATE data/data_cloud_file.h data/data_cloud_themes.cpp data/data_cloud_themes.h - data/data_countries.cpp - data/data_countries.h data/data_document.cpp data/data_document.h data/data_document_media.cpp @@ -803,8 +799,6 @@ PRIVATE passport/passport_panel.h passport/passport_panel_controller.cpp passport/passport_panel_controller.h - passport/passport_panel_details_row.cpp - passport/passport_panel_details_row.h passport/passport_panel_edit_contact.cpp passport/passport_panel_edit_contact.h passport/passport_panel_edit_document.cpp diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 085bb8921..182ffcce2 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "boxes/add_contact_box.h" #include "boxes/confirm_box.h" -#include "boxes/single_choice_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_type_box.h" @@ -20,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_links.h" #include "boxes/peers/edit_linked_chat_box.h" #include "boxes/stickers_box.h" +#include "ui/boxes/single_choice_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "core/application.h" #include "core/core_settings.h" diff --git a/Telegram/SourceFiles/calls/calls_group_settings.cpp b/Telegram/SourceFiles/calls/calls_group_settings.cpp index 42491c72d..4a21d3027 100644 --- a/Telegram/SourceFiles/calls/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/calls_group_settings.cpp @@ -35,7 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" #include "data/data_changes.h" #include "core/application.h" -#include "boxes/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_media_devices.h" #include "settings/settings_common.h" diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 2caf79935..8bc8950d9 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1820,7 +1820,7 @@ Utf8String FormatDateTime( ).toUtf8(); } -Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy) { +Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy) { return Ui::FillAmountAndCurrency( amount, QString::fromUtf8(currency)).toUtf8(); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index ff169de80..694c941db 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -660,7 +660,7 @@ Utf8String FormatDateTime( QChar dateSeparator = QChar('.'), QChar timeSeparator = QChar(':'), QChar separator = QChar(' ')); -Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy); +Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy); Utf8String FormatFileSize(int64 size); Utf8String FormatDuration(int64 seconds); diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 15bced85c..91385fca1 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -958,7 +958,9 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { UpdateComponents(HistoryServicePayment::Bit()); const auto amount = data.vtotal_amount().v; const auto currency = qs(data.vcurrency()); - Get()->amount = Ui::FillAmountAndCurrency(amount, currency); + Get()->amount = Ui::FillAmountAndCurrency( + amount, + currency); } else if (message.vaction().type() == mtpc_messageActionGroupCall) { const auto &data = message.vaction().c_messageActionGroupCall(); if (data.vduration()) { diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 48820b699..3f61abb39 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -61,8 +61,8 @@ PhoneWidget::PhoneWidget( setErrorCentered(true); setupQrLogin(); - if (!_country->onChooseCountry(getData()->country)) { - _country->onChooseCountry(qsl("US")); + if (!_country->chooseCountry(getData()->country)) { + _country->chooseCountry(qsl("US")); } _changed = false; } @@ -251,7 +251,7 @@ QString PhoneWidget::fullNumber() const { } void PhoneWidget::selectCountry(const QString &country) { - _country->onChooseCountry(country); + _country->chooseCountry(country); } void PhoneWidget::setInnerFocus() { diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index a18be2540..6fe13111a 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -9,10 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "passport/passport_panel_edit_document.h" -#include "passport/passport_panel_details_row.h" #include "passport/passport_panel_edit_contact.h" #include "passport/passport_panel_edit_scans.h" #include "passport/passport_panel.h" +#include "passport/ui/passport_details_row.h" #include "base/openssl_help.h" #include "base/unixtime.h" #include "boxes/passcode_box.h" @@ -212,7 +212,7 @@ EditDocumentScheme GetDocumentScheme( result.rows = { { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("first_name"), tr::lng_passport_first_name(tr::now), NameValidate, @@ -221,7 +221,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("middle_name"), tr::lng_passport_middle_name(tr::now), NameOrEmptyValidate, @@ -231,7 +231,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("last_name"), tr::lng_passport_last_name(tr::now), NameValidate, @@ -241,7 +241,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Date, + Ui::PanelDetailsType::Date, qsl("birth_date"), tr::lng_passport_birth_date(tr::now), DateValidate, @@ -249,7 +249,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Gender, + Ui::PanelDetailsType::Gender, qsl("gender"), tr::lng_passport_gender(tr::now), GenderValidate, @@ -257,7 +257,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("country_code"), tr::lng_passport_country(tr::now), CountryValidate, @@ -265,7 +265,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("residence_country_code"), tr::lng_passport_residence_country(tr::now), CountryValidate, @@ -273,7 +273,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Scans, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("document_no"), tr::lng_passport_document_number(tr::now), DocumentValidate, @@ -282,7 +282,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Scans, - PanelDetailsType::Date, + Ui::PanelDetailsType::Date, qsl("expiry_date"), tr::lng_passport_expiry_date(tr::now), DateOrEmptyValidate, @@ -344,7 +344,7 @@ EditDocumentScheme GetDocumentScheme( auto additional = std::initializer_list{ { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("first_name_native"), tr::lng_passport_first_name(tr::now), NativeNameValidate, @@ -355,7 +355,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("middle_name_native"), tr::lng_passport_middle_name(tr::now), NativeNameOrEmptyValidate, @@ -366,7 +366,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("last_name_native"), tr::lng_passport_last_name(tr::now), NativeNameValidate, @@ -411,7 +411,7 @@ EditDocumentScheme GetDocumentScheme( result.rows = { { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("street_line1"), tr::lng_passport_street(tr::now), StreetValidate, @@ -420,7 +420,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("street_line2"), tr::lng_passport_street(tr::now), DontValidate, @@ -429,7 +429,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("city"), tr::lng_passport_city(tr::now), CityValidate, @@ -438,7 +438,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("state"), tr::lng_passport_state(tr::now), DontValidate, @@ -447,7 +447,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("country_code"), tr::lng_passport_country(tr::now), CountryValidate, @@ -455,7 +455,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Postcode, + Ui::PanelDetailsType::Postcode, qsl("post_code"), tr::lng_passport_postcode(tr::now), PostcodeValidate, diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp index 578075696..3b9986ade 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_contact.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index fc018a42c..506f07fe8 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -8,8 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_document.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" #include "passport/passport_panel_edit_scans.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/labels.h" @@ -19,6 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" +#include "data/data_countries.h" +#include "data/data_user.h" // ->bot()->session() +#include "main/main_session.h" // ->session().user() #include "ui/text/text_utilities.h" // Ui::Text::ToUpper #include "boxes/abstract_box.h" #include "boxes/confirm_box.h" @@ -363,7 +366,7 @@ not_null PanelEditDocument::setupContent( const ValueMap &fields) { accumulate_max( maxLabelWidth, - PanelDetailsRow::LabelWidth(row.label)); + Ui::PanelDetailsRow::LabelWidth(row.label)); }); if (maxLabelWidth > 0) { if (error && !error->isEmpty()) { @@ -513,12 +516,20 @@ void PanelEditDocument::createDetailsRow( }; const auto current = valueOrEmpty(fields, row.key); + const auto showBox = [controller = _controller]( + object_ptr box) { + controller->show(std::move(box)); + }; + const auto isoByPhone = Data::CountryISO2ByPhone( + _controller->bot()->session().user()->phone()); + const auto [it, ok] = _details.emplace( i, - container->add(PanelDetailsRow::Create( + container->add(Ui::PanelDetailsRow::Create( container, + showBox, + isoByPhone, row.inputType, - _controller, row.label, maxLabelWidth, current.text, @@ -537,7 +548,7 @@ void PanelEditDocument::createDetailsRow( }, it->second->lifetime()); } -not_null PanelEditDocument::findRow( +not_null PanelEditDocument::findRow( const QString &key) const { for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) { const auto &row = _scheme.rows[i]; @@ -636,7 +647,7 @@ bool PanelEditDocument::validate() { _scroll->scrollToY(_scroll->scrollTop() + scrolldelta); error = firsttop.y(); } - auto first = QPointer(); + auto first = QPointer(); for (const auto &[i, field] : ranges::views::reverse(_details)) { const auto &row = _scheme.rows[i]; if (row.valueClass == Scheme::ValueClass::Additional diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h index bf9dd9f96..aa13908ce 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h @@ -24,15 +24,19 @@ template class SlideWrap; } // namespace Ui +namespace Passport::Ui { +using namespace ::Ui; +enum class PanelDetailsType; +class PanelDetailsRow; +} // namespace Passport::Ui + namespace Passport { class PanelController; struct ValueMap; struct ScanInfo; class EditScans; -class PanelDetailsRow; enum class FileType; -enum class PanelDetailsType; struct ScanListData; struct EditDocumentScheme { @@ -50,7 +54,7 @@ struct EditDocumentScheme { using Validator = Fn(const QString &value)>; using Formatter = Fn; ValueClass valueClass = ValueClass::Fields; - PanelDetailsType inputType = PanelDetailsType(); + Ui::PanelDetailsType inputType = Ui::PanelDetailsType(); QString key; QString label; Validator error; @@ -140,7 +144,7 @@ private: const Scheme::Row &row, const ValueMap &fields, int maxLabelWidth); - not_null findRow(const QString &key) const; + not_null findRow(const QString &key) const; not_null _controller; Scheme _scheme; @@ -151,7 +155,7 @@ private: QPointer _editScans; QPointer> _commonError; - std::map> _details; + std::map> _details; bool _fieldsChanged = false; bool _additionalShown = false; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp index 9ef957d15..88c668408 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_scans.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/box_content_divider.h" diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index 75eeba4c3..5802e6c3a 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_form.h" #include "passport/passport_panel_controller.h" +#include "passport/ui/passport_form_row.h" #include "lang/lang_keys.h" #include "boxes/abstract_box.h" #include "core/click_handler_types.h" @@ -30,145 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Passport { -class PanelForm::Row : public Ui::RippleButton { -public: - explicit Row(QWidget *parent); - - void updateContent( - const QString &title, - const QString &description, - bool ready, - bool error, - anim::type animated); - -protected: - int resizeGetHeight(int newWidth) override; - - void paintEvent(QPaintEvent *e) override; - -private: - int countAvailableWidth() const; - int countAvailableWidth(int newWidth) const; - - Ui::Text::String _title; - Ui::Text::String _description; - int _titleHeight = 0; - int _descriptionHeight = 0; - bool _ready = false; - bool _error = false; - Ui::Animations::Simple _errorAnimation; - -}; - -PanelForm::Row::Row(QWidget *parent) -: RippleButton(parent, st::passportRowRipple) -, _title(st::boxWideWidth / 2) -, _description(st::boxWideWidth / 2) { -} - -void PanelForm::Row::updateContent( - const QString &title, - const QString &description, - bool ready, - bool error, - anim::type animated) { - _title.setText( - st::semiboldTextStyle, - title, - Ui::NameTextOptions()); - _description.setText( - st::defaultTextStyle, - description, - TextParseOptions { - TextParseMultiline, - 0, - 0, - Qt::LayoutDirectionAuto - }); - _ready = ready && !error; - if (_error != error) { - _error = error; - if (animated == anim::type::instant) { - _errorAnimation.stop(); - } else { - _errorAnimation.start( - [=] { update(); }, - _error ? 0. : 1., - _error ? 1. : 0., - st::fadeWrapDuration); - } - } - resizeToWidth(width()); - update(); -} - -int PanelForm::Row::resizeGetHeight(int newWidth) { - const auto availableWidth = countAvailableWidth(newWidth); - _titleHeight = _title.countHeight(availableWidth); - _descriptionHeight = _description.countHeight(availableWidth); - const auto result = st::passportRowPadding.top() - + _titleHeight - + st::passportRowSkip - + _descriptionHeight - + st::passportRowPadding.bottom(); - return result; -} - -int PanelForm::Row::countAvailableWidth(int newWidth) const { - return newWidth - - st::passportRowPadding.left() - - st::passportRowPadding.right() - - (_ready - ? st::passportRowReadyIcon - : st::passportRowEmptyIcon).width() - - st::passportRowIconSkip; -} - -int PanelForm::Row::countAvailableWidth() const { - return countAvailableWidth(width()); -} - -void PanelForm::Row::paintEvent(QPaintEvent *e) { - Painter p(this); - - paintRipple(p, 0, 0); - - const auto left = st::passportRowPadding.left(); - const auto availableWidth = countAvailableWidth(); - auto top = st::passportRowPadding.top(); - - const auto error = _errorAnimation.value(_error ? 1. : 0.); - - p.setPen(st::passportRowTitleFg); - _title.drawLeft(p, left, top, availableWidth, width()); - top += _titleHeight + st::passportRowSkip; - - p.setPen(anim::pen( - st::passportRowDescriptionFg, - st::boxTextFgError, - error)); - _description.drawLeft(p, left, top, availableWidth, width()); - top += _descriptionHeight + st::passportRowPadding.bottom(); - - const auto &icon = _ready - ? st::passportRowReadyIcon - : st::passportRowEmptyIcon; - if (error > 0. && !_ready) { - icon.paint( - p, - width() - st::passportRowPadding.right() - icon.width(), - (height() - icon.height()) / 2, - width(), - anim::color(st::menuIconFgOver, st::boxTextFgError, error)); - } else { - icon.paint( - p, - width() - st::passportRowPadding.right() - icon.width(), - (height() - icon.height()) / 2, - width()); - } -} - PanelForm::PanelForm( QWidget *parent, not_null controller) diff --git a/Telegram/SourceFiles/passport/passport_panel_form.h b/Telegram/SourceFiles/passport/passport_panel_form.h index badc773f0..26b2dfa3b 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.h +++ b/Telegram/SourceFiles/passport/passport_panel_form.h @@ -19,6 +19,11 @@ class FlatLabel; class UserpicButton; } // namespace Ui +namespace Passport::Ui { +using namespace ::Ui; +class FormRow; +} // namespace Passport::Ui + namespace Passport { class PanelController; @@ -33,7 +38,7 @@ protected: void resizeEvent(QResizeEvent *e) override; private: - class Row; + using Row = Ui::FormRow; void setupControls(); not_null setupContent(); diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp similarity index 90% rename from Telegram/SourceFiles/passport/passport_panel_details_row.cpp rename to Telegram/SourceFiles/passport/ui/passport_details_row.cpp index 20b7c9732..a93c30ebd 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp @@ -5,9 +5,8 @@ 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 "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" -#include "passport/passport_panel_controller.h" #include "lang/lang_keys.h" #include "base/platform/base_platform_info.h" #include "ui/widgets/input_fields.h" @@ -15,17 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" -#include "ui/countryinput.h" -#include "main/main_session.h" -#include "data/data_user.h" +#include "ui/layers/box_content.h" +#include "ui/boxes/country_select_box.h" #include "data/data_countries.h" #include "styles/style_layers.h" #include "styles/style_passport.h" -namespace Passport { +#include + +namespace Passport::Ui { namespace { -class PostcodeInput : public Ui::MaskedInputField { +class PostcodeInput : public MaskedInputField { public: PostcodeInput( QWidget *parent, @@ -103,7 +103,8 @@ class CountryRow : public PanelDetailsRow { public: CountryRow( QWidget *parent, - not_null controller, + Fn)> showBox, + const QString &defaultCountry, const QString &label, int maxLabelWidth, const QString &value); @@ -121,15 +122,16 @@ private: void toggleError(bool shown); void errorAnimationCallback(); - not_null _controller; - object_ptr _link; + QString _defaultCountry; + Fn)> _showBox; + object_ptr _link; rpl::variable _value; bool _errorShown = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; -class DateInput final : public Ui::MaskedInputField { +class DateInput final : public MaskedInputField { public: using MaskedInputField::MaskedInputField; @@ -191,21 +193,21 @@ private: int number(const object_ptr &field) const; object_ptr _day; - object_ptr> _separator1; + object_ptr> _separator1; object_ptr _month; - object_ptr> _separator2; + object_ptr> _separator2; object_ptr _year; rpl::variable _value; style::cursor _cursor = style::cur_default; - Ui::Animations::Simple _a_borderShown; + Animations::Simple _a_borderShown; int _borderAnimationStart = 0; - Ui::Animations::Simple _a_borderOpacity; + Animations::Simple _a_borderOpacity; bool _borderVisible = false; - Ui::Animations::Simple _a_error; + Animations::Simple _a_error; bool _error = false; - Ui::Animations::Simple _a_focused; + Animations::Simple _a_focused; bool _focused = false; }; @@ -238,18 +240,18 @@ private: void hideGenderError(); void errorAnimationCallback(); - std::unique_ptr createRadioView( - Ui::RadioView* &weak) const; + std::unique_ptr createRadioView( + RadioView* &weak) const; - std::shared_ptr> _group; - Ui::RadioView *_maleRadio = nullptr; - Ui::RadioView *_femaleRadio = nullptr; - object_ptr> _male; - object_ptr> _female; + std::shared_ptr> _group; + RadioView *_maleRadio = nullptr; + RadioView *_femaleRadio = nullptr; + object_ptr> _male; + object_ptr> _female; rpl::variable _value; bool _errorShown = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; @@ -308,12 +310,14 @@ QString CountryString(const QString &code) { CountryRow::CountryRow( QWidget *parent, - not_null controller, + Fn)> showBox, + const QString &defaultCountry, const QString &label, int maxLabelWidth, const QString &value) : PanelDetailsRow(parent, label, maxLabelWidth) -, _controller(controller) +, _defaultCountry(defaultCountry) +, _showBox(std::move(showBox)) , _link(this, CountryString(value), st::boxLinkButton) , _value(value) { _value.changes( @@ -380,20 +384,23 @@ void CountryRow::errorAnimationCallback() { void CountryRow::chooseCountry() { const auto top = _value.current(); const auto name = Data::CountryNameByISO2(top); - const auto isoByPhone = Data::CountryISO2ByPhone( - _controller->bot()->session().user()->phone()); - const auto box = _controller->show(Box(!name.isEmpty() + const auto country = !name.isEmpty() ? top - : !isoByPhone.isEmpty() - ? isoByPhone - : Platform::SystemCountry(), - CountrySelectBox::Type::Countries)); - connect(box, &CountrySelectBox::countryChosen, this, [=](QString iso) { + : !_defaultCountry.isEmpty() + ? _defaultCountry + : Platform::SystemCountry(); + auto box = Box( + country, + CountrySelectBox::Type::Countries); + const auto raw = box.data(); + raw->countryChosen( + ) | rpl::start_with_next([=](QString iso) { _value = iso; _link->setText(CountryString(iso)); hideCountryError(); - box->closeBox(); - }); + raw->closeBox(); + }, lifetime()); + _showBox(std::move(box)); } QDate ValidateDate(const QString &value) { @@ -528,7 +535,7 @@ DateRow::DateRow( GetDay(value)) , _separator1( this, - object_ptr( + object_ptr( this, QString(" / "), st::passportDetailsSeparator), @@ -540,7 +547,7 @@ DateRow::DateRow( GetMonth(value)) , _separator2( this, - object_ptr( + object_ptr( this, QString(" / "), st::passportDetailsSeparator), @@ -552,7 +559,7 @@ DateRow::DateRow( GetYear(value)) , _value(valueCurrent()) { const auto focused = [=](const object_ptr &field) { - return [this, pointer = Ui::MakeWeak(field.data())]{ + return [this, pointer = MakeWeak(field.data())]{ _borderAnimationStart = pointer->borderAnimationStart() + pointer->x() - _day->x(); @@ -565,15 +572,15 @@ DateRow::DateRow( const auto changed = [=] { _value = valueCurrent(); }; - connect(_day, &Ui::MaskedInputField::focused, focused(_day)); - connect(_month, &Ui::MaskedInputField::focused, focused(_month)); - connect(_year, &Ui::MaskedInputField::focused, focused(_year)); - connect(_day, &Ui::MaskedInputField::blurred, blurred); - connect(_month, &Ui::MaskedInputField::blurred, blurred); - connect(_year, &Ui::MaskedInputField::blurred, blurred); - connect(_day, &Ui::MaskedInputField::changed, changed); - connect(_month, &Ui::MaskedInputField::changed, changed); - connect(_year, &Ui::MaskedInputField::changed, changed); + connect(_day, &MaskedInputField::focused, focused(_day)); + connect(_month, &MaskedInputField::focused, focused(_month)); + connect(_year, &MaskedInputField::focused, focused(_year)); + connect(_day, &MaskedInputField::blurred, blurred); + connect(_month, &MaskedInputField::blurred, blurred); + connect(_year, &MaskedInputField::blurred, blurred); + connect(_day, &MaskedInputField::changed, changed); + connect(_month, &MaskedInputField::changed, changed); + connect(_year, &MaskedInputField::changed, changed); _day->setMaxValue(31); _day->putNext() | rpl::start_with_next([=](QChar ch) { putNext(_month, ch); @@ -845,8 +852,8 @@ GenderRow::GenderRow( const QString &value) : PanelDetailsRow(parent, label, maxLabelWidth) , _group(StringToGender(value).has_value() - ? std::make_shared>(*StringToGender(value)) - : std::make_shared>()) + ? std::make_shared>(*StringToGender(value)) + : std::make_shared>()) , _male( this, _group, @@ -868,9 +875,9 @@ GenderRow::GenderRow( }); } -std::unique_ptr GenderRow::createRadioView( - Ui::RadioView* &weak) const { - auto result = std::make_unique(st::defaultRadio, false); +std::unique_ptr GenderRow::createRadioView( + RadioView* &weak) const { + auto result = std::make_unique(st::defaultRadio, false); weak = result.get(); return result; } @@ -959,8 +966,9 @@ PanelDetailsRow::PanelDetailsRow( object_ptr PanelDetailsRow::Create( QWidget *parent, + Fn)> showBox, + const QString &defaultCountry, Type type, - not_null controller, const QString &label, int maxLabelWidth, const QString &value, @@ -969,7 +977,7 @@ object_ptr PanelDetailsRow::Create( auto result = [&]() -> object_ptr { switch (type) { case Type::Text: - return object_ptr>( + return object_ptr>( parent, label, maxLabelWidth, @@ -985,7 +993,8 @@ object_ptr PanelDetailsRow::Create( case Type::Country: return object_ptr( parent, - controller, + showBox, + defaultCountry, label, maxLabelWidth, value); @@ -1062,7 +1071,7 @@ void PanelDetailsRow::showError(std::optional error) { if (!_error) { _error.create( this, - object_ptr( + object_ptr( this, *error, st::passportVerifyErrorLabel)); @@ -1122,4 +1131,4 @@ void PanelDetailsRow::paintEvent(QPaintEvent *e) { p.drawTextLeft(padding.left(), padding.top(), width(), _label); } -} // namespace Passport +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.h b/Telegram/SourceFiles/passport/ui/passport_details_row.h similarity index 83% rename from Telegram/SourceFiles/passport/passport_panel_details_row.h rename to Telegram/SourceFiles/passport/ui/passport_details_row.h index fc0204967..d93708fe4 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.h +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.h @@ -11,18 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/wrap/padding_wrap.h" #include "ui/widgets/labels.h" -#include "boxes/abstract_box.h" namespace Ui { +class BoxContent; class InputField; class FlatLabel; template class SlideWrap; } // namespace Ui -namespace Passport { +namespace Passport::Ui { -class PanelController; +using namespace ::Ui; enum class PanelDetailsType { Text, @@ -32,7 +32,7 @@ enum class PanelDetailsType { Gender, }; -class PanelDetailsRow : public Ui::RpWidget { +class PanelDetailsRow : public RpWidget { public: using Type = PanelDetailsType; @@ -43,8 +43,9 @@ public: static object_ptr Create( QWidget *parent, + Fn)> showBox, + const QString &defaultCountry, Type type, - not_null controller, const QString &label, int maxLabelWidth, const QString &value, @@ -74,11 +75,11 @@ private: QString _label; int _maxLabelWidth = 0; - object_ptr> _error = { nullptr }; + object_ptr> _error = { nullptr }; bool _errorShown = false; bool _errorHideSubscription = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; -} // namespace Passport +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/ui/passport_form_row.cpp b/Telegram/SourceFiles/passport/ui/passport_form_row.cpp new file mode 100644 index 000000000..6d3104408 --- /dev/null +++ b/Telegram/SourceFiles/passport/ui/passport_form_row.cpp @@ -0,0 +1,125 @@ +/* +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 "passport/ui/passport_form_row.h" + +#include "ui/text/text_options.h" +#include "styles/style_passport.h" +#include "styles/style_layers.h" + +namespace Passport::Ui { + +FormRow::FormRow(QWidget *parent) +: RippleButton(parent, st::passportRowRipple) +, _title(st::boxWideWidth / 2) +, _description(st::boxWideWidth / 2) { +} + +void FormRow::updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated) { + _title.setText( + st::semiboldTextStyle, + title, + NameTextOptions()); + _description.setText( + st::defaultTextStyle, + description, + TextParseOptions { + TextParseMultiline, + 0, + 0, + Qt::LayoutDirectionAuto + }); + _ready = ready && !error; + if (_error != error) { + _error = error; + if (animated == anim::type::instant) { + _errorAnimation.stop(); + } else { + _errorAnimation.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + st::fadeWrapDuration); + } + } + resizeToWidth(width()); + update(); +} + +int FormRow::resizeGetHeight(int newWidth) { + const auto availableWidth = countAvailableWidth(newWidth); + _titleHeight = _title.countHeight(availableWidth); + _descriptionHeight = _description.countHeight(availableWidth); + const auto result = st::passportRowPadding.top() + + _titleHeight + + st::passportRowSkip + + _descriptionHeight + + st::passportRowPadding.bottom(); + return result; +} + +int FormRow::countAvailableWidth(int newWidth) const { + return newWidth + - st::passportRowPadding.left() + - st::passportRowPadding.right() + - (_ready + ? st::passportRowReadyIcon + : st::passportRowEmptyIcon).width() + - st::passportRowIconSkip; +} + +int FormRow::countAvailableWidth() const { + return countAvailableWidth(width()); +} + +void FormRow::paintEvent(QPaintEvent *e) { + Painter p(this); + + paintRipple(p, 0, 0); + + const auto left = st::passportRowPadding.left(); + const auto availableWidth = countAvailableWidth(); + auto top = st::passportRowPadding.top(); + + const auto error = _errorAnimation.value(_error ? 1. : 0.); + + p.setPen(st::passportRowTitleFg); + _title.drawLeft(p, left, top, availableWidth, width()); + top += _titleHeight + st::passportRowSkip; + + p.setPen(anim::pen( + st::passportRowDescriptionFg, + st::boxTextFgError, + error)); + _description.drawLeft(p, left, top, availableWidth, width()); + top += _descriptionHeight + st::passportRowPadding.bottom(); + + const auto &icon = _ready + ? st::passportRowReadyIcon + : st::passportRowEmptyIcon; + if (error > 0. && !_ready) { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width(), + anim::color(st::menuIconFgOver, st::boxTextFgError, error)); + } else { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width()); + } +} + +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/ui/passport_form_row.h b/Telegram/SourceFiles/passport/ui/passport_form_row.h new file mode 100644 index 000000000..2778df204 --- /dev/null +++ b/Telegram/SourceFiles/passport/ui/passport_form_row.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/text/text.h" +#include "ui/effects/animations.h" +#include "ui/widgets/buttons.h" + +namespace Passport::Ui { + +using namespace ::Ui; + +class FormRow : public RippleButton { +public: + explicit FormRow(QWidget *parent); + + void updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + +private: + int countAvailableWidth() const; + int countAvailableWidth(int newWidth) const; + + Text::String _title; + Text::String _description; + int _titleHeight = 0; + int _descriptionHeight = 0; + bool _ready = false; + bool _error = false; + Animations::Simple _errorAnimation; + +}; + +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 916150a20..d0606e4ca 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -95,11 +95,19 @@ not_null CheckoutProcess::panelDelegate() { void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { v::match(update.data, [&](const FormReady &) { - _panel->showForm(_form->invoice()); - }, [&](const FormError &error) { + showForm(); + }, [&](const FormError &error) { // #TODO payments refactor errors handleFormError(error); + }, [&](const ValidateError &error) { + handleValidateError(error); }, [&](const SendError &error) { handleSendError(error); + }, [&](const ValidateFinished &) { + showForm(); + if (_submitState == SubmitState::Validation) { + _submitState = SubmitState::Validated; + panelSubmit(); + } }, [&](const VerificationNeeded &info) { if (_webviewWindow) { _webviewWindow->navigate(info.url); @@ -130,8 +138,44 @@ void CheckoutProcess::handleFormError(const FormError &error) { } else if (type == u"INVOICE_ALREADY_PAID"_q) { } - App::wnd()->activate(); - Ui::Toast::Show("payments.getPaymentForm: " + type); + if (_panel) { + _panel->showToast("payments.getPaymentForm: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.getPaymentForm: " + type); + } +} + +void CheckoutProcess::handleValidateError(const ValidateError &error) { + // #TODO payments errors + const auto &type = error.type; + if (type == u"REQ_INFO_NAME_INVALID"_q) { + + } else if (type == u"REQ_INFO_EMAIL_INVALID"_q) { + + } else if (type == u"REQ_INFO_PHONE_INVALID"_q) { + + } else if (type == u"ADDRESS_STREET_LINE1_INVALID"_q) { + + } else if (type == u"ADDRESS_CITY_INVALID"_q) { + + } else if (type == u"ADDRESS_STATE_INVALID"_q) { + + } else if (type == u"ADDRESS_COUNTRY_INVALID"_q) { + + } else if (type == u"ADDRESS_POSTCODE_INVALID"_q) { + + } else if (type == u"SHIPPING_BOT_TIMEOUT"_q) { + + } else if (type == u"SHIPPING_NOT_AVAILABLE"_q) { + + } + if (_panel) { + _panel->showToast("payments.validateRequestedInfo: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.validateRequestedInfo: " + type); + } } void CheckoutProcess::handleSendError(const SendError &error) { @@ -150,8 +194,12 @@ void CheckoutProcess::handleSendError(const SendError &error) { } else if (type == u"BOT_PRECHECKOUT_FAILED"_q) { } - App::wnd()->activate(); - Ui::Toast::Show("payments.sendPaymentForm: " + type); + if (_panel) { + _panel->showToast("payments.sendPaymentForm: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.sendPaymentForm: " + type); + } } void CheckoutProcess::panelRequestClose() { @@ -176,6 +224,26 @@ void CheckoutProcess::panelCloseSure() { } void CheckoutProcess::panelSubmit() { + if (_submitState == SubmitState::Validation + || _submitState == SubmitState::Finishing) { + return; + } + const auto &invoice = _form->invoice(); + const auto &options = _form->shippingOptions(); + if (!options.list.empty() && options.selectedId.isEmpty()) { + chooseShippingOption(); + return; + } else if (_submitState != SubmitState::Validated + && options.list.empty() + && (invoice.isShippingAddressRequested + || invoice.isNameRequested + || invoice.isEmailRequested + || invoice.isPhoneRequested)) { + _submitState = SubmitState::Validation; + _form->validateInformation(_form->savedInformation()); + return; + } + _submitState = SubmitState::Finishing; _webviewWindow = std::make_unique( _form->details().url, panelDelegate()); @@ -237,4 +305,62 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { return false; } +void CheckoutProcess::panelEditShippingInformation() { + showEditInformation(Ui::EditField::ShippingInformation); +} + +void CheckoutProcess::panelEditName() { + showEditInformation(Ui::EditField::Name); +} + +void CheckoutProcess::panelEditEmail() { + showEditInformation(Ui::EditField::Email); +} + +void CheckoutProcess::panelEditPhone() { + showEditInformation(Ui::EditField::Phone); +} + +void CheckoutProcess::showForm() { + _panel->showForm( + _form->invoice(), + _form->savedInformation(), + _form->shippingOptions()); +} + +void CheckoutProcess::showEditInformation(Ui::EditField field) { + if (_submitState != SubmitState::None) { + return; + } + _panel->showEditInformation( + _form->invoice(), + _form->savedInformation(), + field); +} + +void CheckoutProcess::chooseShippingOption() { + _panel->chooseShippingOption(_form->shippingOptions()); +} + +void CheckoutProcess::panelChooseShippingOption() { + if (_submitState != SubmitState::None) { + return; + } + chooseShippingOption(); +} + +void CheckoutProcess::panelChangeShippingOption(const QString &id) { + _form->setShippingOption(id); + showForm(); +} + +void CheckoutProcess::panelValidateInformation( + Ui::RequestedInformation data) { + _form->validateInformation(data); +} + +void CheckoutProcess::panelShowBox(object_ptr box) { + _panel->showBox(std::move(box)); +} + } // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 1eff37db9..7c130f9d5 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -19,6 +19,7 @@ class Session; namespace Payments::Ui { class Panel; class WebviewWindow; +enum class EditField; } // namespace Payments::Ui namespace Payments { @@ -27,6 +28,7 @@ class Form; struct FormUpdate; struct FormError; struct SendError; +struct ValidateError; class CheckoutProcess final : public base::has_weak_ptr @@ -45,22 +47,44 @@ public: void requestActivate(); private: + enum class SubmitState { + None, + Validation, + Validated, + Finishing, + }; [[nodiscard]] not_null panelDelegate(); void handleFormUpdate(const FormUpdate &update); void handleFormError(const FormError &error); + void handleValidateError(const ValidateError &error); void handleSendError(const SendError &error); + void showForm(); + void showEditInformation(Ui::EditField field); + void chooseShippingOption(); + void panelRequestClose() override; void panelCloseSure() override; void panelSubmit() override; void panelWebviewMessage(const QJsonDocument &message) override; bool panelWebviewNavigationAttempt(const QString &uri) override; + void panelEditShippingInformation() override; + void panelEditName() override; + void panelEditEmail() override; + void panelEditPhone() override; + void panelChooseShippingOption() override; + void panelChangeShippingOption(const QString &id) override; + + void panelValidateInformation(Ui::RequestedInformation data) override; + void panelShowBox(object_ptr box) override; + const not_null _session; const std::unique_ptr _form; const std::unique_ptr _panel; std::unique_ptr _webviewWindow; + SubmitState _submitState = SubmitState::None; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 397a5058f..cc60acb9a 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -27,16 +27,53 @@ namespace { }); } +[[nodiscard]] std::vector ParsePrices( + const MTPVector &data) { + return ranges::views::all( + data.v + ) | ranges::views::transform([](const MTPLabeledPrice &price) { + return price.match([&](const MTPDlabeledPrice &data) { + return Ui::LabeledPrice{ + .label = qs(data.vlabel()), + .price = *reinterpret_cast(&data.vamount().v), + }; + }); + }) | ranges::to_vector; +} + +[[nodiscard]] MTPPaymentRequestedInfo Serialize( + const Ui::RequestedInformation &information) { + using Flag = MTPDpaymentRequestedInfo::Flag; + return MTP_paymentRequestedInfo( + MTP_flags((information.name.isEmpty() ? Flag(0) : Flag::f_name) + | (information.email.isEmpty() ? Flag(0) : Flag::f_email) + | (information.phone.isEmpty() ? Flag(0) : Flag::f_phone) + | (information.shippingAddress + ? Flag::f_shipping_address + : Flag(0))), + MTP_string(information.name), + MTP_string(information.phone), + MTP_string(information.email), + MTP_postAddress( + MTP_string(information.shippingAddress.address1), + MTP_string(information.shippingAddress.address2), + MTP_string(information.shippingAddress.city), + MTP_string(information.shippingAddress.state), + MTP_string(information.shippingAddress.countryIso2), + MTP_string(information.shippingAddress.postCode))); +} + } // namespace Form::Form(not_null session, FullMsgId itemId) : _session(session) +, _api(&_session->mtp()) , _msgId(itemId.msg) { requestForm(); } void Form::requestForm() { - _session->api().request(MTPpayments_GetPaymentForm( + _api.request(MTPpayments_GetPaymentForm( MTP_int(_msgId) )).done([=](const MTPpayments_PaymentForm &result) { result.match([&](const auto &data) { @@ -69,18 +106,8 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { } 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, + .prices = ParsePrices(data.vprices()), .currency = qs(data.vcurrency()), .isNameRequested = data.is_name_requested(), @@ -115,7 +142,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); - _savedInformation = Ui::SavedInformation{ + _savedInformation = Ui::RequestedInformation{ .name = qs(data.vname().value_or_empty()), .phone = qs(data.vphone().value_or_empty()), .email = qs(data.vemail().value_or_empty()), @@ -132,11 +159,17 @@ void Form::processSavedCredentials( } void Form::send(const QByteArray &serializedCredentials) { - _session->api().request(MTPpayments_SendPaymentForm( - MTP_flags(0), + using Flag = MTPpayments_SendPaymentForm::Flag; + _api.request(MTPpayments_SendPaymentForm( + MTP_flags((_requestedInformationId.isEmpty() + ? Flag(0) + : Flag::f_requested_info_id) + | (_shippingOptions.selectedId.isEmpty() + ? Flag(0) + : Flag::f_shipping_option_id)), MTP_int(_msgId), - MTPstring(), // requested_info_id - MTPstring(), // shipping_option_id, + MTP_string(_requestedInformationId), + MTP_string(_shippingOptions.selectedId), MTP_inputPaymentCredentials( MTP_flags(0), MTP_dataJSON(MTP_bytes(serializedCredentials))) @@ -151,4 +184,59 @@ void Form::send(const QByteArray &serializedCredentials) { }).send(); } +void Form::validateInformation(const Ui::RequestedInformation &information) { + if (_validateRequestId) { + if (_validatedInformation == information) { + return; + } + _api.request(base::take(_validateRequestId)).cancel(); + } + _validatedInformation = information; + _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( + MTP_flags(0), // #TODO payments save information + MTP_int(_msgId), + Serialize(information) + )).done([=](const MTPpayments_ValidatedRequestedInfo &result) { + _validateRequestId = 0; + const auto oldSelectedId = _shippingOptions.selectedId; + result.match([&](const MTPDpayments_validatedRequestedInfo &data) { + _requestedInformationId = data.vid().value_or_empty(); + processShippingOptions( + data.vshipping_options().value_or_empty()); + }); + _shippingOptions.selectedId = ranges::contains( + _shippingOptions.list, + oldSelectedId, + &Ui::ShippingOption::id + ) ? oldSelectedId : QString(); + if (_shippingOptions.selectedId.isEmpty() + && _shippingOptions.list.size() == 1) { + _shippingOptions.selectedId = _shippingOptions.list.front().id; + } + _savedInformation = _validatedInformation; + _updates.fire({ ValidateFinished{} }); + }).fail([=](const MTP::Error &error) { + _validateRequestId = 0; + _updates.fire({ ValidateError{ error.type() } }); + }).send(); +} + +void Form::setShippingOption(const QString &id) { + _shippingOptions.selectedId = id; +} + +void Form::processShippingOptions(const QVector &data) { + _shippingOptions = Ui::ShippingOptions{ ranges::views::all( + data + ) | ranges::views::transform([](const MTPShippingOption &option) { + return option.match([](const MTPDshippingOption &data) { + return Ui::ShippingOption{ + .id = qs(data.vid()), + .title = qs(data.vtitle()), + .prices = ParsePrices(data.vprices()), + }; + }); + }) | ranges::to_vector }; +} + } // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 92288605c..c553ab5c2 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "payments/ui/payments_panel_data.h" +#include "mtproto/sender.h" namespace Main { class Session; @@ -34,10 +35,16 @@ struct FormDetails { struct FormReady {}; +struct ValidateFinished {}; + struct FormError { QString type; }; +struct ValidateError { + QString type; +}; + struct SendError { QString type; }; @@ -54,8 +61,10 @@ struct FormUpdate { std::variant< FormReady, FormError, + ValidateError, SendError, VerificationNeeded, + ValidateFinished, PaymentFinished> data; }; @@ -69,17 +78,22 @@ public: [[nodiscard]] const FormDetails &details() const { return _details; } - [[nodiscard]] const Ui::SavedInformation &savedInformation() const { + [[nodiscard]] const Ui::RequestedInformation &savedInformation() const { return _savedInformation; } [[nodiscard]] const Ui::SavedCredentials &savedCredentials() const { return _savedCredentials; } + [[nodiscard]] const Ui::ShippingOptions &shippingOptions() const { + return _shippingOptions; + } [[nodiscard]] rpl::producer updates() const { return _updates.events(); } + void validateInformation(const Ui::RequestedInformation &information); + void setShippingOption(const QString &id); void send(const QByteArray &serializedCredentials); private: @@ -90,15 +104,23 @@ private: void processSavedInformation(const MTPDpaymentRequestedInfo &data); void processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data); + void processShippingOptions(const QVector &data); const not_null _session; + MTP::Sender _api; MsgId _msgId = 0; Ui::Invoice _invoice; FormDetails _details; - Ui::SavedInformation _savedInformation; + Ui::RequestedInformation _savedInformation; Ui::SavedCredentials _savedCredentials; + Ui::RequestedInformation _validatedInformation; + mtpRequestId _validateRequestId = 0; + + Ui::ShippingOptions _shippingOptions; + QString _requestedInformationId; + rpl::event_stream _updates; }; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 6e99489eb..95700e28c 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -8,3 +8,5 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL using "ui/basic.style"; using "passport/passport.style"; + +paymentsFormPricePadding: margins(22px, 7px, 22px, 6px); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp new file mode 100644 index 000000000..08026b585 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -0,0 +1,262 @@ +/* +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_edit_information.h" + +#include "payments/ui/payments_panel_delegate.h" +#include "passport/ui/passport_details_row.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.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 { +namespace { + +constexpr auto kMaxStreetSize = 64; +constexpr auto kMaxPostcodeSize = 10; +constexpr auto kMaxNameSize = 64; +constexpr auto kMaxEmailSize = 128; +constexpr auto kMaxPhoneSize = 16; + +} // namespace + +EditInformation::EditInformation( + QWidget *parent, + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field, + not_null delegate) +: _delegate(delegate) +, _invoice(invoice) +, _information(current) +, _scroll(this, st::passportPanelScroll) +, _topShadow(this) +, _bottomShadow(this) +, _done( + this, + tr::lng_about_done(), + st::passportPanelSaveValue) { + setupControls(); +} + +void EditInformation::setupControls() { + const auto inner = setupContent(); + + _done->addClickHandler([=] { + _delegate->panelValidateInformation(collect()); + }); + + 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 EditInformation::setupContent() { + const auto inner = _scroll->setOwnedWidget( + object_ptr(this)); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + inner->resizeToWidth(width); + }, inner->lifetime()); + + const auto showBox = [=](object_ptr box) { + _delegate->panelShowBox(std::move(box)); + }; + using Type = Passport::Ui::PanelDetailsType; + auto maxLabelWidth = 0; + if (_invoice.isShippingAddressRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_street(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_city(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_state(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_country(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_postcode(tr::now))); + } + if (_invoice.isNameRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_name(tr::now))); + } + if (_invoice.isEmailRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_email(tr::now))); + } + if (_invoice.isPhoneRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_phone(tr::now))); + } + if (_invoice.isShippingAddressRequested) { + _street1 = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_street(tr::now), + maxLabelWidth, + _information.shippingAddress.address1, + QString(), + kMaxStreetSize)); + _street2 = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_street(tr::now), + maxLabelWidth, + _information.shippingAddress.address2, + QString(), + kMaxStreetSize)); + _city = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_city(tr::now), + maxLabelWidth, + _information.shippingAddress.city, + QString(), + kMaxStreetSize)); + _state = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_state(tr::now), + maxLabelWidth, + _information.shippingAddress.state, + QString(), + kMaxStreetSize)); + _country = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Country, + tr::lng_passport_country(tr::now), + maxLabelWidth, + _information.shippingAddress.countryIso2, + QString())); + _postcode = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Postcode, + tr::lng_passport_postcode(tr::now), + maxLabelWidth, + _information.shippingAddress.postCode, + QString(), + kMaxPostcodeSize)); + //StreetValidate, // #TODO payments + //CityValidate, + //CountryValidate, + //CountryFormat, + //PostcodeValidate, + } + if (_invoice.isNameRequested) { + _name = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_name(tr::now), + maxLabelWidth, + _information.name, + QString(), + kMaxNameSize)); + } + if (_invoice.isEmailRequested) { + _email = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_email(tr::now), + maxLabelWidth, + _information.email, + QString(), + kMaxEmailSize)); + } + if (_invoice.isPhoneRequested) { + _phone = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_phone(tr::now), + maxLabelWidth, + _information.phone, + QString(), + kMaxPhoneSize)); + } + return inner; +} + +void EditInformation::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void EditInformation::updateControlsGeometry() { + const auto submitTop = height() - _done->height(); + _scroll->setGeometry(0, 0, width(), submitTop); + _topShadow->resizeToWidth(width()); + _topShadow->moveToLeft(0, 0); + _bottomShadow->resizeToWidth(width()); + _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); + _done->setFullWidth(width()); + _done->moveToLeft(0, submitTop); + + _scroll->updateBars(); +} + +RequestedInformation EditInformation::collect() const { + return { + .name = _name ? _name->valueCurrent() : QString(), + .phone = _phone ? _phone->valueCurrent() : QString(), + .email = _email ? _email->valueCurrent() : QString(), + .shippingAddress = { + .address1 = _street1 ? _street1->valueCurrent() : QString(), + .address2 = _street2 ? _street2->valueCurrent() : QString(), + .city = _city ? _city->valueCurrent() : QString(), + .state = _state ? _state->valueCurrent() : QString(), + .countryIso2 = _country ? _country->valueCurrent() : QString(), + .postCode = _postcode ? _postcode->valueCurrent() : QString(), + }, + }; +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h new file mode 100644 index 000000000..fecd70736 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -0,0 +1,71 @@ +/* +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 Passport::Ui { +class PanelDetailsRow; +} // namespace Passport::Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +class EditInformation final : public RpWidget { +public: + EditInformation( + QWidget *parent, + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field, + not_null delegate); + +private: + using Row = Passport::Ui::PanelDetailsRow; + + void resizeEvent(QResizeEvent *e) override; + + void setupControls(); + [[nodiscard]] not_null setupContent(); + void updateControlsGeometry(); + + [[nodiscard]] RequestedInformation collect() const; + + const not_null _delegate; + Invoice _invoice; + RequestedInformation _information; + + object_ptr _scroll; + object_ptr _topShadow; + object_ptr _bottomShadow; + object_ptr _done; + + Row *_street1 = nullptr; + Row *_street2 = nullptr; + Row *_city = nullptr; + Row *_state = nullptr; + Row *_country = nullptr; + Row *_postcode = nullptr; + Row *_name = nullptr; + Row *_email = nullptr; + Row *_phone = nullptr; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 935aa7cdd..03656e6b2 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -8,10 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_form_summary.h" #include "payments/ui/payments_panel_delegate.h" +#include "passport/ui/passport_form_row.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" +#include "ui/text/format_values.h" #include "lang/lang_keys.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -19,24 +22,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Payments::Ui { using namespace ::Ui; +using namespace Passport::Ui; class PanelDelegate; FormSummary::FormSummary( QWidget *parent, const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options, not_null delegate) : _delegate(delegate) +, _invoice(invoice) +, _options(options) +, _information(current) , _scroll(this, st::passportPanelScroll) , _topShadow(this) , _bottomShadow(this) , _submit( this, - tr::lng_payments_pay_amount(lt_amount, rpl::single(QString("much"))), + tr::lng_payments_pay_amount( + lt_amount, + rpl::single(computeTotalAmount())), st::passportPanelAuthorize) { setupControls(); } +QString FormSummary::computeAmount(int64 amount) const { + return FillAmountAndCurrency(amount, _invoice.currency); +} + +QString FormSummary::computeTotalAmount() const { + const auto total = ranges::accumulate( + _invoice.prices, + int64(0), + std::plus<>(), + &LabeledPrice::price); + const auto selected = ranges::find( + _options.list, + _options.selectedId, + &ShippingOption::id); + const auto shipping = (selected != end(_options.list)) + ? ranges::accumulate( + selected->prices, + int64(0), + std::plus<>(), + &LabeledPrice::price) + : int64(0); + return computeAmount(total + shipping); +} + void FormSummary::setupControls() { const auto inner = setupContent(); @@ -64,6 +99,116 @@ not_null FormSummary::setupContent() { inner->resizeToWidth(width); }, inner->lifetime()); + for (const auto &price : _invoice.prices) { + inner->add( + object_ptr( + inner, + price.label + ": " + computeAmount(price.price), + st::passportFormPolicy), + st::paymentsFormPricePadding); + } + const auto selected = ranges::find( + _options.list, + _options.selectedId, + &ShippingOption::id); + if (selected != end(_options.list)) { + for (const auto &price : selected->prices) { + inner->add( + object_ptr( + inner, + price.label + ": " + computeAmount(price.price), + st::passportFormPolicy), + st::paymentsFormPricePadding); + } + } + inner->add( + object_ptr( + inner, + "Total: " + computeTotalAmount(), + st::passportFormHeader), + st::passportFormHeaderPadding); + + inner->add( + object_ptr( + inner, + st::passportFormDividerHeight), + { 0, 0, 0, st::passportFormHeaderPadding.top() }); + + if (_invoice.isShippingAddressRequested) { + const auto info = inner->add(object_ptr(inner)); + info->addClickHandler([=] { + _delegate->panelEditShippingInformation(); + }); + auto list = QStringList(); + const auto push = [&](const QString &value) { + if (!value.isEmpty()) { + list.push_back(value); + } + }; + push(_information.shippingAddress.address1); + push(_information.shippingAddress.address2); + push(_information.shippingAddress.city); + push(_information.shippingAddress.state); + push(_information.shippingAddress.countryIso2); + push(_information.shippingAddress.postCode); + info->updateContent( + tr::lng_payments_shipping_address(tr::now), + (list.isEmpty() ? "enter pls" : list.join(", ")), + !list.isEmpty(), + false, + anim::type::instant); + } + if (!_options.list.empty()) { + const auto options = inner->add(object_ptr(inner)); + options->addClickHandler([=] { + _delegate->panelChooseShippingOption(); + }); + options->updateContent( + tr::lng_payments_shipping_method(tr::now), + (selected != end(_options.list) + ? selected->title + : "enter pls"), + (selected != end(_options.list)), + false, + anim::type::instant); + } + if (_invoice.isNameRequested) { + const auto name = inner->add(object_ptr(inner)); + name->addClickHandler([=] { _delegate->panelEditName(); }); + name->updateContent( + tr::lng_payments_info_name(tr::now), + (_information.name.isEmpty() + ? "enter pls" + : _information.name), + !_information.name.isEmpty(), + false, + anim::type::instant); + } + if (_invoice.isEmailRequested) { + const auto email = inner->add(object_ptr(inner)); + email->addClickHandler([=] { _delegate->panelEditEmail(); }); + email->updateContent( + tr::lng_payments_info_email(tr::now), + (_information.email.isEmpty() + ? "enter pls" + : _information.email), + !_information.email.isEmpty(), + false, + anim::type::instant); + } + if (_invoice.isPhoneRequested) { + const auto phone = inner->add(object_ptr(inner)); + phone->addClickHandler([=] { _delegate->panelEditPhone(); }); + phone->updateContent( + tr::lng_payments_info_email(tr::now), + (_information.phone.isEmpty() + ? "enter pls" + : _information.phone), + !_information.phone.isEmpty(), + false, + anim::type::instant); + } + return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 39ca9e7f0..32dd89bc7 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -28,6 +28,8 @@ public: FormSummary( QWidget *parent, const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options, not_null delegate); private: @@ -37,7 +39,13 @@ private: [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); + [[nodiscard]] QString computeAmount(int64 amount) const; + [[nodiscard]] QString computeTotalAmount() const; + const not_null _delegate; + Invoice _invoice; + ShippingOptions _options; + RequestedInformation _information; object_ptr _scroll; object_ptr _topShadow; object_ptr _bottomShadow; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 7c5ab32fd..22a977c87 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -8,8 +8,10 @@ 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_edit_information.h" #include "payments/ui/payments_panel_delegate.h" #include "ui/widgets/separate_panel.h" +#include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -39,9 +41,63 @@ void Panel::requestActivate() { _widget->showAndActivate(); } -void Panel::showForm(const Invoice &invoice) { +void Panel::showForm( + const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options) { _widget->showInner( - base::make_unique_q(_widget.get(), invoice, _delegate)); + base::make_unique_q( + _widget.get(), + invoice, + current, + options, + _delegate)); +} + +void Panel::showEditInformation( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field) { + _widget->showInner(base::make_unique_q( + _widget.get(), + invoice, + current, + field, + _delegate)); +} + +void Panel::chooseShippingOption(const ShippingOptions &options) { + showBox(Box([=](not_null box) { + auto list = options.list | ranges::views::transform( + &ShippingOption::title + ) | ranges::to_vector; + const auto i = ranges::find( + options.list, + options.selectedId, + &ShippingOption::id); + const auto save = [=](int option) { + _delegate->panelChangeShippingOption(options.list[option].id); + }; + SingleChoiceBox(box, { + .title = tr::lng_payments_shipping_method(), + .options = list, + .initialSelection = (i != end(options.list) + ? (i - begin(options.list)) + : -1), + .callback = save, + }); + })); +} + +void Panel::showBox(object_ptr box) { + _widget->showBox( + std::move(box), + Ui::LayerOption::KeepOther, + anim::type::normal); +} + +void Panel::showToast(const QString &text) { + _widget->showToast(text); } } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 0aa68c456..102f0fa2c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -7,8 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/object_ptr.h" + namespace Ui { class SeparatePanel; +class BoxContent; } // namespace Ui namespace Payments::Ui { @@ -17,6 +20,9 @@ using namespace ::Ui; class PanelDelegate; struct Invoice; +struct RequestedInformation; +struct ShippingOptions; +enum class EditField; class Panel final { public: @@ -25,7 +31,18 @@ public: void requestActivate(); - void showForm(const Invoice &invoice); + void showForm( + const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options); + void showEditInformation( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field); + void chooseShippingOption(const ShippingOptions &options); + + void showBox(object_ptr box); + void showToast(const QString &text); private: const not_null _delegate; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 735136350..f282bcbc9 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -11,7 +11,7 @@ namespace Payments::Ui { struct LabeledPrice { QString label; - uint64 price = 0; + int64 price = 0; }; struct Invoice { @@ -36,6 +36,17 @@ struct Invoice { } }; +struct ShippingOption { + QString id; + QString title; + std::vector prices; +}; + +struct ShippingOptions { + std::vector list; + QString selectedId; +}; + struct Address { QString address1; QString address2; @@ -52,9 +63,21 @@ struct Address { [[nodiscard]] explicit operator bool() const { return valid(); } + + inline bool operator==(const Address &other) const { + return (address1 == other.address1) + && (address2 == other.address2) + && (city == other.city) + && (state == other.state) + && (countryIso2 == other.countryIso2) + && (postCode == other.postCode); + } + inline bool operator!=(const Address &other) const { + return !(*this == other); + } }; -struct SavedInformation { +struct RequestedInformation { QString name; QString phone; QString email; @@ -69,6 +92,16 @@ struct SavedInformation { [[nodiscard]] explicit operator bool() const { return !empty(); } + + inline bool operator==(const RequestedInformation &other) const { + return (name == other.name) + && (phone == other.phone) + && (email == other.email) + && (shippingAddress == other.shippingAddress); + } + inline bool operator!=(const RequestedInformation &other) const { + return !(*this == other); + } }; struct SavedCredentials { @@ -83,4 +116,11 @@ struct SavedCredentials { } }; +enum class EditField { + ShippingInformation, + Name, + Email, + Phone, +}; + } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index 49cc5b988..f737dc2b4 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -7,11 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/object_ptr.h" + class QJsonDocument; class QString; +namespace Ui { +class BoxContent; +} // namespace Ui + namespace Payments::Ui { +using namespace ::Ui; + +struct RequestedInformation; + class PanelDelegate { public: virtual void panelRequestClose() = 0; @@ -19,6 +29,16 @@ public: virtual void panelSubmit() = 0; virtual void panelWebviewMessage(const QJsonDocument &message) = 0; virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; + + virtual void panelEditShippingInformation() = 0; + virtual void panelEditName() = 0; + virtual void panelEditEmail() = 0; + virtual void panelEditPhone() = 0; + virtual void panelChooseShippingOption() = 0; + virtual void panelChangeShippingOption(const QString &id) = 0; + + virtual void panelValidateInformation(RequestedInformation data) = 0; + virtual void panelShowBox(object_ptr box) = 0; }; } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 4faea034e..3a6b277c0 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/level_meter.h" #include "ui/widgets/buttons.h" -#include "boxes/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "boxes/confirm_box.h" #include "platform/platform_specific.h" #include "main/main_session.h" diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp new file mode 100644 index 000000000..33b8139a2 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp @@ -0,0 +1,443 @@ +/* +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 "ui/boxes/country_select_box.h" + +#include "lang/lang_keys.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/multi_select.h" +#include "ui/effects/ripple_animation.h" +#include "data/data_countries.h" +#include "base/qt_adapters.h" +#include "styles/style_layers.h" +#include "styles/style_boxes.h" +#include "styles/style_intro.h" + +#include + +namespace Ui { +namespace { + +QString LastValidISO; + +} // namespace + +class CountrySelectBox::Inner : public TWidget { +public: + Inner(QWidget *parent, Type type); + ~Inner(); + + void updateFilter(QString filter = QString()); + + void selectSkip(int32 dir); + void selectSkipPage(int32 h, int32 dir); + + void chooseCountry(); + + void refresh(); + + [[nodiscard]] rpl::producer countryChosen() const { + return _countryChosen.events(); + } + + [[nodiscard]] rpl::producer mustScrollTo() const { + return _mustScrollTo.events(); + } + +protected: + void paintEvent(QPaintEvent *e) override; + void enterEventHook(QEvent *e) override; + void leaveEventHook(QEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + +private: + void updateSelected() { + updateSelected(mapFromGlobal(QCursor::pos())); + } + void updateSelected(QPoint localPos); + void updateSelectedRow(); + void updateRow(int index); + void setPressed(int pressed); + const std::vector> ¤t() const; + + Type _type = Type::Phones; + int _rowHeight = 0; + + int _selected = -1; + int _pressed = -1; + QString _filter; + bool _mouseSelection = false; + + std::vector> _ripples; + + std::vector> _list; + std::vector> _filtered; + base::flat_map> _byLetter; + std::vector> _namesList; + + rpl::event_stream _countryChosen; + rpl::event_stream _mustScrollTo; + +}; + +CountrySelectBox::CountrySelectBox(QWidget*) +: _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { +} + +CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) +: _type(type) +, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { + if (Data::CountriesByISO2().contains(iso)) { + LastValidISO = iso; + } +} + +rpl::producer CountrySelectBox::countryChosen() const { + return _inner->countryChosen(); +} + +void CountrySelectBox::prepare() { + setTitle(tr::lng_country_select()); + + _select->resizeToWidth(st::boxWidth); + _select->setQueryChangedCallback([=](const QString &query) { + applyFilterUpdate(query); + }); + _select->setSubmittedCallback([=](Qt::KeyboardModifiers) { + submit(); + }); + + _inner = setInnerWidget( + object_ptr(this, _type), + st::countriesScroll, + _select->height()); + + addButton(tr::lng_close(), [=] { closeBox(); }); + + setDimensions(st::boxWidth, st::boxMaxListHeight); + + _inner->mustScrollTo( + ) | rpl::start_with_next([=](ScrollToRequest request) { + onScrollToY(request.ymin, request.ymax); + }, lifetime()); +} + +void CountrySelectBox::submit() { + _inner->chooseCountry(); +} + +void CountrySelectBox::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Down) { + _inner->selectSkip(1); + } else if (e->key() == Qt::Key_Up) { + _inner->selectSkip(-1); + } else if (e->key() == Qt::Key_PageDown) { + _inner->selectSkipPage(height() - _select->height(), 1); + } else if (e->key() == Qt::Key_PageUp) { + _inner->selectSkipPage(height() - _select->height(), -1); + } else { + BoxContent::keyPressEvent(e); + } +} + +void CountrySelectBox::resizeEvent(QResizeEvent *e) { + BoxContent::resizeEvent(e); + + _select->resizeToWidth(width()); + _select->moveToLeft(0, 0); + + _inner->resizeToWidth(width()); +} + +void CountrySelectBox::applyFilterUpdate(const QString &query) { + onScrollToY(0); + _inner->updateFilter(query); +} + +void CountrySelectBox::setInnerFocus() { + _select->setInnerFocus(); +} + +CountrySelectBox::Inner::Inner(QWidget *parent, Type type) +: TWidget(parent) +, _type(type) +, _rowHeight(st::countryRowHeight) { + setAttribute(Qt::WA_OpaquePaintEvent); + + const auto &byISO2 = Data::CountriesByISO2(); + + _list.reserve(byISO2.size()); + _namesList.reserve(byISO2.size()); + + const auto l = byISO2.constFind(LastValidISO); + const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr; + if (lastValid) { + _list.emplace_back(lastValid); + } + for (const auto &entry : Data::Countries()) { + if (&entry != lastValid) { + _list.emplace_back(&entry); + } + } + auto index = 0; + for (const auto info : _list) { + auto full = QString::fromUtf8(info->name) + + ' ' + + (info->alternativeName + ? QString::fromUtf8(info->alternativeName) + : QString()); + const auto namesList = std::move(full).toLower().split( + QRegularExpression("[\\s\\-]"), + base::QStringSkipEmptyParts); + auto &names = _namesList.emplace_back(); + names.reserve(namesList.size()); + for (const auto &name : namesList) { + const auto part = name.trimmed(); + if (part.isEmpty()) { + continue; + } + + const auto ch = part[0]; + auto &byLetter = _byLetter[ch]; + if (byLetter.empty() || byLetter.back() != index) { + byLetter.push_back(index); + } + names.push_back(part); + } + ++index; + } + + _filter = u"a"_q; + updateFilter(); +} + +void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) { + Painter p(this); + QRect r(e->rect()); + p.setClipRect(r); + + const auto &list = current(); + if (list.empty()) { + p.fillRect(r, st::boxBg); + p.setFont(st::noContactsFont); + p.setPen(st::noContactsColor); + p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center); + return; + } + const auto l = int(list.size()); + if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) { + p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg); + } + int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l); + int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l); + for (int32 i = from; i < to; ++i) { + auto selected = (i == (_pressed >= 0 ? _pressed : _selected)); + auto y = st::countriesSkip + i * _rowHeight; + + p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg); + if (_ripples.size() > i && _ripples[i]) { + _ripples[i]->paint(p, 0, y, width()); + if (_ripples[i]->empty()) { + _ripples[i].reset(); + } + } + + auto code = QString("+") + list[i]->code; + auto codeWidth = st::countryRowCodeFont->width(code); + + auto name = QString::fromUtf8(list[i]->name); + auto nameWidth = st::countryRowNameFont->width(name); + auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width; + if (nameWidth > availWidth) { + name = st::countryRowNameFont->elided(name, availWidth); + nameWidth = st::countryRowNameFont->width(name); + } + + p.setFont(st::countryRowNameFont); + p.setPen(st::countryRowNameFg); + p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name); + + if (_type == Type::Phones) { + p.setFont(st::countryRowCodeFont); + p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg); + p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code); + } + } +} + +void CountrySelectBox::Inner::enterEventHook(QEvent *e) { + setMouseTracking(true); +} + +void CountrySelectBox::Inner::leaveEventHook(QEvent *e) { + _mouseSelection = false; + setMouseTracking(false); + if (_selected >= 0) { + updateSelectedRow(); + _selected = -1; + } +} + +void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) { + _mouseSelection = true; + updateSelected(e->pos()); +} + +void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) { + _mouseSelection = true; + updateSelected(e->pos()); + + setPressed(_selected); + const auto &list = current(); + if (_pressed >= 0 && _pressed < list.size()) { + if (_ripples.size() <= _pressed) { + _ripples.reserve(_pressed + 1); + while (_ripples.size() <= _pressed) { + _ripples.push_back(nullptr); + } + } + if (!_ripples[_pressed]) { + auto mask = RippleAnimation::rectMask(QSize(width(), _rowHeight)); + _ripples[_pressed] = std::make_unique(st::countryRipple, std::move(mask), [this, index = _pressed] { + updateRow(index); + }); + _ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight)); + } + } +} + +void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) { + auto pressed = _pressed; + setPressed(-1); + updateSelectedRow(); + if (e->button() == Qt::LeftButton) { + if ((pressed >= 0) && pressed == _selected) { + chooseCountry(); + } + } +} + +void CountrySelectBox::Inner::updateFilter(QString filter) { + const auto words = TextUtilities::PrepareSearchWords(filter); + filter = words.isEmpty() ? QString() : words.join(' '); + if (_filter == filter) { + return; + } + _filter = filter; + + const auto findWord = [&]( + const std::vector &names, + const QString &word) { + for (const auto &name : names) { + if (name.startsWith(word)) { + return true; + } + } + return false; + }; + const auto hasAllWords = [&](const std::vector &names) { + for (const auto &word : words) { + if (!findWord(names, word)) { + return false; + } + } + return true; + }; + if (!_filter.isEmpty()) { + _filtered.clear(); + for (const auto index : _byLetter[_filter[0].toLower()]) { + const auto &names = _namesList[index]; + if (hasAllWords(_namesList[index])) { + _filtered.push_back(_list[index]); + } + } + } + refresh(); + _selected = current().empty() ? -1 : 0; + update(); +} + +void CountrySelectBox::Inner::selectSkip(int32 dir) { + _mouseSelection = false; + + const auto &list = current(); + int cur = (_selected >= 0) ? _selected : -1; + cur += dir; + if (cur <= 0) { + _selected = list.empty() ? -1 : 0; + } else if (cur >= list.size()) { + _selected = -1; + } else { + _selected = cur; + } + if (_selected >= 0) { + _mustScrollTo.fire(ScrollToRequest( + st::countriesSkip + _selected * _rowHeight, + st::countriesSkip + (_selected + 1) * _rowHeight)); + } + update(); +} + +void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) { + int32 points = h / _rowHeight; + if (!points) return; + selectSkip(points * dir); +} + +void CountrySelectBox::Inner::chooseCountry() { + const auto &list = current(); + _countryChosen.fire((_selected >= 0 && _selected < list.size()) + ? QString(list[_selected]->iso2) + : QString()); +} + +void CountrySelectBox::Inner::refresh() { + const auto &list = current(); + resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip)); +} + +void CountrySelectBox::Inner::updateSelected(QPoint localPos) { + if (!_mouseSelection) return; + + auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos())); + + const auto &list = current(); + auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1; + if (_selected != selected) { + updateSelectedRow(); + _selected = selected; + updateSelectedRow(); + } +} + +auto CountrySelectBox::Inner::current() const +-> const std::vector> & { + return _filter.isEmpty() ? _list : _filtered; +} + +void CountrySelectBox::Inner::updateSelectedRow() { + updateRow(_selected); +} + +void CountrySelectBox::Inner::updateRow(int index) { + if (index >= 0) { + update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight); + } +} + +void CountrySelectBox::Inner::setPressed(int pressed) { + if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) { + _ripples[_pressed]->lastStop(); + } + _pressed = pressed; +} + +CountrySelectBox::Inner::~Inner() = default; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.h b/Telegram/SourceFiles/ui/boxes/country_select_box.h new file mode 100644 index 000000000..ba580a342 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.h @@ -0,0 +1,53 @@ +/* +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 "boxes/abstract_box.h" +#include "styles/style_widgets.h" + +namespace Data { +struct CountryInfo; +} // namespace Data + +namespace Ui { + +class MultiSelect; +class RippleAnimation; + +class CountrySelectBox : public BoxContent { +public: + enum class Type { + Phones, + Countries, + }; + + CountrySelectBox(QWidget*); + CountrySelectBox(QWidget*, const QString &iso, Type type); + + [[nodiscard]] rpl::producer countryChosen() const; + +protected: + void prepare() override; + void setInnerFocus() override; + + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + +private: + void submit(); + void applyFilterUpdate(const QString &query); + + Type _type = Type::Phones; + object_ptr _select; + + class Inner; + QPointer _inner; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/single_choice_box.cpp b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp similarity index 94% rename from Telegram/SourceFiles/boxes/single_choice_box.cpp rename to Telegram/SourceFiles/ui/boxes/single_choice_box.cpp index af788ecf6..6b1ea3e5f 100644 --- a/Telegram/SourceFiles/boxes/single_choice_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp @@ -5,11 +5,9 @@ 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 "boxes/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" -#include "storage/localstorage.h" -#include "mainwindow.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" diff --git a/Telegram/SourceFiles/boxes/single_choice_box.h b/Telegram/SourceFiles/ui/boxes/single_choice_box.h similarity index 100% rename from Telegram/SourceFiles/boxes/single_choice_box.h rename to Telegram/SourceFiles/ui/boxes/single_choice_box.h diff --git a/Telegram/SourceFiles/ui/countryinput.cpp b/Telegram/SourceFiles/ui/countryinput.cpp index e06e3fc6e..f9871a6bd 100644 --- a/Telegram/SourceFiles/ui/countryinput.cpp +++ b/Telegram/SourceFiles/ui/countryinput.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/multi_select.h" #include "ui/effects/ripple_animation.h" +#include "ui/boxes/country_select_box.h" #include "data/data_countries.h" #include "base/qt_adapters.h" #include "styles/style_layers.h" @@ -23,7 +24,8 @@ QString LastValidISO; } // namespace -CountryInput::CountryInput(QWidget *parent, const style::InputField &st) : TWidget(parent) +CountryInput::CountryInput(QWidget *parent, const style::InputField &st) +: RpWidget(parent) , _st(st) , _text(tr::lng_country_code(tr::now)) { resize(_st.width, _st.heightMin); @@ -91,8 +93,11 @@ void CountryInput::mouseMoveEvent(QMouseEvent *e) { void CountryInput::mousePressEvent(QMouseEvent *e) { mouseMoveEvent(e); if (_active) { - auto box = Ui::show(Box()); - connect(box, SIGNAL(countryChosen(const QString&)), this, SLOT(onChooseCountry(const QString&))); + auto box = Ui::show(Box()); + box->countryChosen( + ) | rpl::start_with_next([=](QString iso) { + chooseCountry(iso); + }, lifetime()); } } @@ -125,7 +130,7 @@ void CountryInput::onChooseCode(const QString &code) { update(); } -bool CountryInput::onChooseCountry(const QString &iso) { +bool CountryInput::chooseCountry(const QString &iso) { Ui::hideLayer(); const auto &byISO2 = Data::CountriesByISO2(); @@ -146,349 +151,3 @@ bool CountryInput::onChooseCountry(const QString &iso) { void CountryInput::setText(const QString &newText) { _text = _st.font->elided(newText, width() - _st.textMargins.left() - _st.textMargins.right()); } - -CountrySelectBox::CountrySelectBox(QWidget*) -: _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { -} - -CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) -: _type(type) -, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { - if (Data::CountriesByISO2().contains(iso)) { - LastValidISO = iso; - } -} - -void CountrySelectBox::prepare() { - setTitle(tr::lng_country_select()); - - _select->resizeToWidth(st::boxWidth); - _select->setQueryChangedCallback([=](const QString &query) { - applyFilterUpdate(query); - }); - _select->setSubmittedCallback([=](Qt::KeyboardModifiers) { - submit(); - }); - - _inner = setInnerWidget( - object_ptr(this, _type), - st::countriesScroll, - _select->height()); - - addButton(tr::lng_close(), [=] { closeBox(); }); - - setDimensions(st::boxWidth, st::boxMaxListHeight); - - connect(_inner, SIGNAL(mustScrollTo(int, int)), this, SLOT(onScrollToY(int, int))); - connect(_inner, SIGNAL(countryChosen(const QString&)), this, SIGNAL(countryChosen(const QString&))); -} - -void CountrySelectBox::submit() { - _inner->chooseCountry(); -} - -void CountrySelectBox::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Down) { - _inner->selectSkip(1); - } else if (e->key() == Qt::Key_Up) { - _inner->selectSkip(-1); - } else if (e->key() == Qt::Key_PageDown) { - _inner->selectSkipPage(height() - _select->height(), 1); - } else if (e->key() == Qt::Key_PageUp) { - _inner->selectSkipPage(height() - _select->height(), -1); - } else { - BoxContent::keyPressEvent(e); - } -} - -void CountrySelectBox::resizeEvent(QResizeEvent *e) { - BoxContent::resizeEvent(e); - - _select->resizeToWidth(width()); - _select->moveToLeft(0, 0); - - _inner->resizeToWidth(width()); -} - -void CountrySelectBox::applyFilterUpdate(const QString &query) { - onScrollToY(0); - _inner->updateFilter(query); -} - -void CountrySelectBox::setInnerFocus() { - _select->setInnerFocus(); -} - -CountrySelectBox::Inner::Inner(QWidget *parent, Type type) -: TWidget(parent) -, _type(type) -, _rowHeight(st::countryRowHeight) { - setAttribute(Qt::WA_OpaquePaintEvent); - - const auto &byISO2 = Data::CountriesByISO2(); - - _list.reserve(byISO2.size()); - _namesList.reserve(byISO2.size()); - - const auto l = byISO2.constFind(LastValidISO); - const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr; - if (lastValid) { - _list.emplace_back(lastValid); - } - for (const auto &entry : Data::Countries()) { - if (&entry != lastValid) { - _list.emplace_back(&entry); - } - } - auto index = 0; - for (const auto info : _list) { - auto full = QString::fromUtf8(info->name) - + ' ' - + (info->alternativeName - ? QString::fromUtf8(info->alternativeName) - : QString()); - const auto namesList = std::move(full).toLower().split( - QRegularExpression("[\\s\\-]"), - base::QStringSkipEmptyParts); - auto &names = _namesList.emplace_back(); - names.reserve(namesList.size()); - for (const auto &name : namesList) { - const auto part = name.trimmed(); - if (part.isEmpty()) { - continue; - } - - const auto ch = part[0]; - auto &byLetter = _byLetter[ch]; - if (byLetter.empty() || byLetter.back() != index) { - byLetter.push_back(index); - } - names.push_back(part); - } - ++index; - } - - _filter = qsl("a"); - updateFilter(); -} - -void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) { - Painter p(this); - QRect r(e->rect()); - p.setClipRect(r); - - const auto &list = current(); - if (list.empty()) { - p.fillRect(r, st::boxBg); - p.setFont(st::noContactsFont); - p.setPen(st::noContactsColor); - p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center); - return; - } - const auto l = list.size(); - if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) { - p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg); - } - int32 from = floorclamp(r.y() - st::countriesSkip, _rowHeight, 0, l); - int32 to = ceilclamp(r.y() + r.height() - st::countriesSkip, _rowHeight, 0, l); - for (int32 i = from; i < to; ++i) { - auto selected = (i == (_pressed >= 0 ? _pressed : _selected)); - auto y = st::countriesSkip + i * _rowHeight; - - p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg); - if (_ripples.size() > i && _ripples[i]) { - _ripples[i]->paint(p, 0, y, width()); - if (_ripples[i]->empty()) { - _ripples[i].reset(); - } - } - - auto code = QString("+") + list[i]->code; - auto codeWidth = st::countryRowCodeFont->width(code); - - auto name = QString::fromUtf8(list[i]->name); - auto nameWidth = st::countryRowNameFont->width(name); - auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width; - if (nameWidth > availWidth) { - name = st::countryRowNameFont->elided(name, availWidth); - nameWidth = st::countryRowNameFont->width(name); - } - - p.setFont(st::countryRowNameFont); - p.setPen(st::countryRowNameFg); - p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name); - - if (_type == Type::Phones) { - p.setFont(st::countryRowCodeFont); - p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg); - p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code); - } - } -} - -void CountrySelectBox::Inner::enterEventHook(QEvent *e) { - setMouseTracking(true); -} - -void CountrySelectBox::Inner::leaveEventHook(QEvent *e) { - _mouseSelection = false; - setMouseTracking(false); - if (_selected >= 0) { - updateSelectedRow(); - _selected = -1; - } -} - -void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) { - _mouseSelection = true; - updateSelected(e->pos()); -} - -void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) { - _mouseSelection = true; - updateSelected(e->pos()); - - setPressed(_selected); - const auto &list = current(); - if (_pressed >= 0 && _pressed < list.size()) { - if (_ripples.size() <= _pressed) { - _ripples.reserve(_pressed + 1); - while (_ripples.size() <= _pressed) { - _ripples.push_back(nullptr); - } - } - if (!_ripples[_pressed]) { - auto mask = Ui::RippleAnimation::rectMask(QSize(width(), _rowHeight)); - _ripples[_pressed] = std::make_unique(st::countryRipple, std::move(mask), [this, index = _pressed] { - updateRow(index); - }); - _ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight)); - } - } -} - -void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) { - auto pressed = _pressed; - setPressed(-1); - updateSelectedRow(); - if (e->button() == Qt::LeftButton) { - if ((pressed >= 0) && pressed == _selected) { - chooseCountry(); - } - } -} - -void CountrySelectBox::Inner::updateFilter(QString filter) { - const auto words = TextUtilities::PrepareSearchWords(filter); - filter = words.isEmpty() ? QString() : words.join(' '); - if (_filter == filter) { - return; - } - _filter = filter; - - const auto findWord = [&]( - const std::vector &names, - const QString &word) { - for (const auto &name : names) { - if (name.startsWith(word)) { - return true; - } - } - return false; - }; - const auto hasAllWords = [&](const std::vector &names) { - for (const auto &word : words) { - if (!findWord(names, word)) { - return false; - } - } - return true; - }; - if (!_filter.isEmpty()) { - _filtered.clear(); - for (const auto index : _byLetter[_filter[0].toLower()]) { - const auto &names = _namesList[index]; - if (hasAllWords(_namesList[index])) { - _filtered.push_back(_list[index]); - } - } - } - refresh(); - _selected = current().empty() ? -1 : 0; - update(); -} - -void CountrySelectBox::Inner::selectSkip(int32 dir) { - _mouseSelection = false; - - const auto &list = current(); - int cur = (_selected >= 0) ? _selected : -1; - cur += dir; - if (cur <= 0) { - _selected = list.empty() ? -1 : 0; - } else if (cur >= list.size()) { - _selected = -1; - } else { - _selected = cur; - } - if (_selected >= 0) { - mustScrollTo(st::countriesSkip + _selected * _rowHeight, st::countriesSkip + (_selected + 1) * _rowHeight); - } - update(); -} - -void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) { - int32 points = h / _rowHeight; - if (!points) return; - selectSkip(points * dir); -} - -void CountrySelectBox::Inner::chooseCountry() { - const auto &list = current(); - countryChosen((_selected >= 0 && _selected < list.size()) - ? QString(list[_selected]->iso2) - : QString()); -} - -void CountrySelectBox::Inner::refresh() { - const auto &list = current(); - resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip)); -} - -void CountrySelectBox::Inner::updateSelected(QPoint localPos) { - if (!_mouseSelection) return; - - auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos())); - - const auto &list = current(); - auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1; - if (_selected != selected) { - updateSelectedRow(); - _selected = selected; - updateSelectedRow(); - } -} - -auto CountrySelectBox::Inner::current() const --> const std::vector> & { - return _filter.isEmpty() ? _list : _filtered; -} - -void CountrySelectBox::Inner::updateSelectedRow() { - updateRow(_selected); -} - -void CountrySelectBox::Inner::updateRow(int index) { - if (index >= 0) { - update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight); - } -} - -void CountrySelectBox::Inner::setPressed(int pressed) { - if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) { - _ripples[_pressed]->lastStop(); - } - _pressed = pressed; -} - -CountrySelectBox::Inner::~Inner() = default; diff --git a/Telegram/SourceFiles/ui/countryinput.h b/Telegram/SourceFiles/ui/countryinput.h index ffd1a707e..7b0050bc0 100644 --- a/Telegram/SourceFiles/ui/countryinput.h +++ b/Telegram/SourceFiles/ui/countryinput.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "boxes/abstract_box.h" +#include "ui/rp_widget.h" #include "styles/style_widgets.h" namespace Data { @@ -19,19 +19,19 @@ class MultiSelect; class RippleAnimation; } // namespace Ui -class CountryInput : public TWidget { +class CountryInput : public Ui::RpWidget { Q_OBJECT public: CountryInput(QWidget *parent, const style::InputField &st); - QString iso() const { + [[nodiscard]] QString iso() const { return _chosenIso; } + bool chooseCountry(const QString &country); public Q_SLOTS: void onChooseCode(const QString &code); - bool onChooseCountry(const QString &country); Q_SIGNALS: void codeChanged(const QString &code); @@ -53,94 +53,3 @@ private: QPainterPath _placeholderPath; }; - -class CountrySelectBox : public Ui::BoxContent { - Q_OBJECT - -public: - enum class Type { - Phones, - Countries, - }; - - CountrySelectBox(QWidget*); - CountrySelectBox(QWidget*, const QString &iso, Type type); - -Q_SIGNALS: - void countryChosen(const QString &iso); - -protected: - void prepare() override; - void setInnerFocus() override; - - void keyPressEvent(QKeyEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - -private: - void submit(); - void applyFilterUpdate(const QString &query); - - Type _type = Type::Phones; - object_ptr _select; - - class Inner; - QPointer _inner; - -}; - -// This class is hold in header because it requires Qt preprocessing. -class CountrySelectBox::Inner : public TWidget { - Q_OBJECT - -public: - Inner(QWidget *parent, Type type); - - void updateFilter(QString filter = QString()); - - void selectSkip(int32 dir); - void selectSkipPage(int32 h, int32 dir); - - void chooseCountry(); - - void refresh(); - - ~Inner(); - -Q_SIGNALS: - void countryChosen(const QString &iso); - void mustScrollTo(int ymin, int ymax); - -protected: - void paintEvent(QPaintEvent *e) override; - void enterEventHook(QEvent *e) override; - void leaveEventHook(QEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - -private: - void updateSelected() { - updateSelected(mapFromGlobal(QCursor::pos())); - } - void updateSelected(QPoint localPos); - void updateSelectedRow(); - void updateRow(int index); - void setPressed(int pressed); - const std::vector> ¤t() const; - - Type _type = Type::Phones; - int _rowHeight = 0; - - int _selected = -1; - int _pressed = -1; - QString _filter; - bool _mouseSelection = false; - - std::vector> _ripples; - - std::vector> _list; - std::vector> _filtered; - base::flat_map> _byLetter; - std::vector> _namesList; - -}; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index 9cb1e10ba..b614fe14c 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -125,7 +125,7 @@ QString FormatPlayedText(qint64 played, qint64 duration) { return tr::lng_duration_played(tr::now, lt_played, FormatDurationText(played), lt_duration, FormatDurationText(duration)); } -QString FillAmountAndCurrency(uint64 amount, const QString ¤cy) { +QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { static const auto ShortCurrencyNames = QMap{ { u"USD"_q, QString::fromUtf8("\x24") }, { u"GBP"_q, QString::fromUtf8("\xC2\xA3") }, diff --git a/Telegram/SourceFiles/ui/text/format_values.h b/Telegram/SourceFiles/ui/text/format_values.h index 15f9b0211..779d3e7ce 100644 --- a/Telegram/SourceFiles/ui/text/format_values.h +++ b/Telegram/SourceFiles/ui/text/format_values.h @@ -24,7 +24,7 @@ inline constexpr auto FileStatusSizeFailed = 0x7FFFFFF2; [[nodiscard]] QString FormatPlayedText(qint64 played, qint64 duration); [[nodiscard]] QString FillAmountAndCurrency( - uint64 amount, + int64 amount, const QString ¤cy); [[nodiscard]] QString ComposeNameString( diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.h b/Telegram/SourceFiles/ui/widgets/multi_select.h index 679a4c8f4..0a0e48680 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.h +++ b/Telegram/SourceFiles/ui/widgets/multi_select.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "base/object_ptr.h" +#include + namespace Ui { class InputField; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 57338bf69..de3044487 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -52,6 +52,9 @@ PRIVATE core/mime_type.cpp core/mime_type.h + data/data_countries.cpp + data/data_countries.h + media/clip/media_clip_check_streaming.cpp media/clip/media_clip_check_streaming.h media/clip/media_clip_ffmpeg.cpp @@ -61,6 +64,13 @@ PRIVATE media/clip/media_clip_reader.cpp media/clip/media_clip_reader.h + passport/ui/passport_details_row.cpp + passport/ui/passport_details_row.h + passport/ui/passport_form_row.cpp + passport/ui/passport_form_row.h + + payments/ui/payments_edit_information.cpp + payments/ui/payments_edit_information.h payments/ui/payments_form_summary.cpp payments/ui/payments_form_summary.h payments/ui/payments_panel.cpp @@ -80,10 +90,14 @@ PRIVATE ui/boxes/calendar_box.h ui/boxes/choose_date_time.cpp ui/boxes/choose_date_time.h + ui/boxes/country_select_box.cpp + ui/boxes/country_select_box.h ui/boxes/edit_invite_link.cpp ui/boxes/edit_invite_link.h ui/boxes/report_box.cpp ui/boxes/report_box.h + ui/boxes/single_choice_box.cpp + ui/boxes/single_choice_box.h ui/chat/attach/attach_album_thumbnail.cpp ui/chat/attach/attach_album_thumbnail.h ui/chat/attach/attach_album_preview.cpp From 212497413cc6fdb04494d8000f6a0a2017dbc2f7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 24 Mar 2021 15:30:01 +0400 Subject: [PATCH 013/127] Show some payment errors, focus fields. --- .../SourceFiles/passport/passport_panel.cpp | 2 +- .../payments/payments_checkout_process.cpp | 172 ++++++++++-------- .../payments/payments_checkout_process.h | 11 +- .../SourceFiles/payments/payments_form.cpp | 10 +- Telegram/SourceFiles/payments/payments_form.h | 28 +-- .../SourceFiles/payments/ui/payments.style | 6 + .../payments/ui/payments_edit_information.cpp | 39 +++- .../payments/ui/payments_edit_information.h | 7 + .../payments/ui/payments_form_summary.cpp | 6 +- .../payments/ui/payments_panel.cpp | 34 +++- .../SourceFiles/payments/ui/payments_panel.h | 12 +- .../payments/ui/payments_panel_data.h | 10 +- .../payments/ui/payments_webview.cpp | 5 +- .../payments/ui/payments_webview.h | 1 + .../SourceFiles/storage/storage_domain.cpp | 4 + Telegram/SourceFiles/storage/storage_domain.h | 2 + .../ui/boxes/country_select_box.cpp | 13 +- .../SourceFiles/ui/boxes/country_select_box.h | 1 + .../SourceFiles/ui/widgets/separate_panel.cpp | 9 +- .../SourceFiles/ui/widgets/separate_panel.h | 2 +- 20 files changed, 244 insertions(+), 130 deletions(-) diff --git a/Telegram/SourceFiles/passport/passport_panel.cpp b/Telegram/SourceFiles/passport/passport_panel.cpp index 0779886c7..da4df7ced 100644 --- a/Telegram/SourceFiles/passport/passport_panel.cpp +++ b/Telegram/SourceFiles/passport/passport_panel.cpp @@ -101,7 +101,7 @@ void Panel::showBox( } void Panel::showToast(const QString &text) { - _widget->showToast(text); + _widget->showToast({ text }); } Panel::~Panel() = default; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index d0606e4ca..ea7aff424 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_webview.h" #include "main/main_session.h" #include "main/main_account.h" +#include "main/main_domain.h" +#include "storage/storage_domain.h" #include "history/history_item.h" #include "history/history.h" #include "core/local_url_handlers.h" // TryConvertUrlToLocal. @@ -19,7 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL // #TODO payments errors #include "mainwindow.h" -#include "ui/toast/toast.h" +#include "ui/toasts/common_toasts.h" #include #include @@ -80,6 +82,10 @@ CheckoutProcess::CheckoutProcess( ) | rpl::start_with_next([=](const FormUpdate &update) { handleFormUpdate(update); }, _lifetime); + _panel->backRequests( + ) | rpl::start_with_next([=] { + showForm(); + }, _panel->lifetime()); } CheckoutProcess::~CheckoutProcess() { @@ -96,12 +102,6 @@ not_null CheckoutProcess::panelDelegate() { void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { v::match(update.data, [&](const FormReady &) { showForm(); - }, [&](const FormError &error) { // #TODO payments refactor errors - handleFormError(error); - }, [&](const ValidateError &error) { - handleValidateError(error); - }, [&](const SendError &error) { - handleSendError(error); }, [&](const ValidateFinished &) { showForm(); if (_submitState == SubmitState::Validation) { @@ -113,6 +113,7 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _webviewWindow->navigate(info.url); } else { _webviewWindow = std::make_unique( + webviewDataPath(), info.url, panelDelegate()); if (!_webviewWindow->shown()) { @@ -125,80 +126,78 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { if (weak) { panelCloseSure(); } + }, [&](const Error &error) { + handleError(error); }); } -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) { - - } - if (_panel) { - _panel->showToast("payments.getPaymentForm: " + type); - } else { - App::wnd()->activate(); - Ui::Toast::Show("payments.getPaymentForm: " + type); - } -} - -void CheckoutProcess::handleValidateError(const ValidateError &error) { - // #TODO payments errors - const auto &type = error.type; - if (type == u"REQ_INFO_NAME_INVALID"_q) { - - } else if (type == u"REQ_INFO_EMAIL_INVALID"_q) { - - } else if (type == u"REQ_INFO_PHONE_INVALID"_q) { - - } else if (type == u"ADDRESS_STREET_LINE1_INVALID"_q) { - - } else if (type == u"ADDRESS_CITY_INVALID"_q) { - - } else if (type == u"ADDRESS_STATE_INVALID"_q) { - - } else if (type == u"ADDRESS_COUNTRY_INVALID"_q) { - - } else if (type == u"ADDRESS_POSTCODE_INVALID"_q) { - - } else if (type == u"SHIPPING_BOT_TIMEOUT"_q) { - - } else if (type == u"SHIPPING_NOT_AVAILABLE"_q) { - - } - if (_panel) { - _panel->showToast("payments.validateRequestedInfo: " + type); - } else { - App::wnd()->activate(); - Ui::Toast::Show("payments.validateRequestedInfo: " + 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) { - - } - if (_panel) { - _panel->showToast("payments.sendPaymentForm: " + type); - } else { - App::wnd()->activate(); - Ui::Toast::Show("payments.sendPaymentForm: " + type); +void CheckoutProcess::handleError(const Error &error) { + const auto showToast = [&](const TextWithEntities &text) { + if (_panel) { + _panel->requestActivate(); + _panel->showToast(text); + } else { + App::wnd()->activate(); + Ui::ShowMultilineToast({ .text = text }); + } + }; + const auto &id = error.id; + switch (error.type) { + case Error::Type::Form: + if (id == u"INVOICE_ALREADY_PAID"_q) { + showToast({ "Already Paid!" }); // #TODO payments errors message + } else if (true + || id == u"PROVIDER_ACCOUNT_INVALID"_q + || id == u"PROVIDER_ACCOUNT_TIMEOUT"_q) { + showToast({ "Error: " + id }); + } + break; + case Error::Type::Validate: + if (_submitState == SubmitState::Validation) { + _submitState = SubmitState::None; + } + if (id == u"REQ_INFO_NAME_INVALID"_q) { + showEditError(Ui::EditField::Name); + } else if (id == u"REQ_INFO_EMAIL_INVALID"_q) { + showEditError(Ui::EditField::Email); + } else if (id == u"REQ_INFO_PHONE_INVALID"_q) { + showEditError(Ui::EditField::Phone); + } else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) { + showEditError(Ui::EditField::ShippingStreet); + } else if (id == u"ADDRESS_CITY_INVALID"_q) { + showEditError(Ui::EditField::ShippingCity); + } else if (id == u"ADDRESS_STATE_INVALID"_q) { + showEditError(Ui::EditField::ShippingState); + } else if (id == u"ADDRESS_COUNTRY_INVALID"_q) { + showEditError(Ui::EditField::ShippingCountry); + } else if (id == u"ADDRESS_POSTCODE_INVALID"_q) { + showEditError(Ui::EditField::ShippingPostcode); + } else if (id == u"SHIPPING_BOT_TIMEOUT"_q) { + showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message + } else if (id == u"SHIPPING_NOT_AVAILABLE"_q) { + showToast({ "Error: Shipping to the selected country is not available!" }); // #TODO payments errors message + } else { + showToast({ "Error: " + id }); + } + break; + case Error::Type::Send: + if (_submitState == SubmitState::Finishing) { + _submitState = SubmitState::None; + } + if (id == u"PAYMENT_FAILED"_q) { + showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message + } else if (id == u"BOT_PRECHECKOUT_FAILED"_q) { + showToast({ "Error: PreCheckout Failed. Your card has not been billed." }); // #TODO payments errors message + } else if (id == u"INVOICE_ALREADY_PAID"_q) { + showToast({ "Already Paid!" }); // #TODO payments errors message + } else if (id == u"REQUESTED_INFO_INVALID"_q + || id == u"SHIPPING_OPTION_INVALID"_q + || id == u"PAYMENT_CREDENTIALS_INVALID"_q + || id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) { + showToast({ "Error: " + id + ". Your card has not been billed." }); + } + break; + default: Unexpected("Error type in CheckoutProcess::handleError."); } } @@ -245,6 +244,7 @@ void CheckoutProcess::panelSubmit() { } _submitState = SubmitState::Finishing; _webviewWindow = std::make_unique( + webviewDataPath(), _form->details().url, panelDelegate()); if (!_webviewWindow->shown()) { @@ -306,7 +306,7 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { } void CheckoutProcess::panelEditShippingInformation() { - showEditInformation(Ui::EditField::ShippingInformation); + showEditInformation(Ui::EditField::ShippingStreet); } void CheckoutProcess::panelEditName() { @@ -338,6 +338,16 @@ void CheckoutProcess::showEditInformation(Ui::EditField field) { field); } +void CheckoutProcess::showEditError(Ui::EditField field) { + if (_submitState != SubmitState::None) { + return; + } + _panel->showEditError( + _form->invoice(), + _form->savedInformation(), + field); +} + void CheckoutProcess::chooseShippingOption() { _panel->chooseShippingOption(_form->shippingOptions()); } @@ -363,4 +373,8 @@ void CheckoutProcess::panelShowBox(object_ptr box) { _panel->showBox(std::move(box)); } +QString CheckoutProcess::webviewDataPath() const { + return _session->domain().local().webviewDataPath(); +} + } // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 7c130f9d5..65d09d986 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -26,9 +26,7 @@ namespace Payments { class Form; struct FormUpdate; -struct FormError; -struct SendError; -struct ValidateError; +struct Error; class CheckoutProcess final : public base::has_weak_ptr @@ -56,14 +54,15 @@ private: [[nodiscard]] not_null panelDelegate(); void handleFormUpdate(const FormUpdate &update); - void handleFormError(const FormError &error); - void handleValidateError(const ValidateError &error); - void handleSendError(const SendError &error); + void handleError(const Error &error); void showForm(); void showEditInformation(Ui::EditField field); + void showEditError(Ui::EditField field); void chooseShippingOption(); + [[nodiscard]] QString webviewDataPath() const; + void panelRequestClose() override; void panelCloseSure() override; void panelSubmit() override; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index cc60acb9a..3e1a06869 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -22,7 +22,7 @@ namespace { .city = qs(data.vcity()), .state = qs(data.vstate()), .countryIso2 = qs(data.vcountry_iso2()), - .postCode = qs(data.vpost_code()), + .postcode = qs(data.vpost_code()), }; }); } @@ -60,7 +60,7 @@ namespace { MTP_string(information.shippingAddress.city), MTP_string(information.shippingAddress.state), MTP_string(information.shippingAddress.countryIso2), - MTP_string(information.shippingAddress.postCode))); + MTP_string(information.shippingAddress.postcode))); } } // namespace @@ -80,7 +80,7 @@ void Form::requestForm() { processForm(data); }); }).fail([=](const MTP::Error &error) { - _updates.fire({ FormError{ error.type() } }); + _updates.fire({ Error{ Error::Type::Form, error.type() } }); }).send(); } @@ -180,7 +180,7 @@ void Form::send(const QByteArray &serializedCredentials) { _updates.fire({ VerificationNeeded{ qs(data.vurl()) } }); }); }).fail([=](const MTP::Error &error) { - _updates.fire({ SendError{ error.type() } }); + _updates.fire({ Error{ Error::Type::Send, error.type() } }); }).send(); } @@ -217,7 +217,7 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _updates.fire({ ValidateFinished{} }); }).fail([=](const MTP::Error &error) { _validateRequestId = 0; - _updates.fire({ ValidateError{ error.type() } }); + _updates.fire({ Error{ Error::Type::Validate, error.type() } }); }).send(); } diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index c553ab5c2..565340654 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -34,25 +34,19 @@ struct FormDetails { }; struct FormReady {}; - struct ValidateFinished {}; - -struct FormError { - QString type; +struct Error { + enum class Type { + Form, + Validate, + Send, + }; + Type type = Type::Form; + QString id; }; - -struct ValidateError { - QString type; -}; - -struct SendError { - QString type; -}; - struct VerificationNeeded { QString url; }; - struct PaymentFinished { MTPUpdates updates; }; @@ -60,12 +54,10 @@ struct PaymentFinished { struct FormUpdate { std::variant< FormReady, - FormError, - ValidateError, - SendError, VerificationNeeded, ValidateFinished, - PaymentFinished> data; + PaymentFinished, + Error> data; }; class Form final { diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 95700e28c..d331d3033 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -10,3 +10,9 @@ using "ui/basic.style"; using "passport/passport.style"; paymentsFormPricePadding: margins(22px, 7px, 22px, 6px); +paymentsPanelSubmit: RoundButton(passportPasswordSubmit) { + width: 0px; + height: 49px; + padding: margins(0px, -3px, 0px, 0px); + textTop: 16px; +} diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 08026b585..100a873d0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -48,6 +48,21 @@ EditInformation::EditInformation( setupControls(); } +void EditInformation::setFocus(EditField field) { + _focusField = field; + if (const auto control = controlForField(field)) { + _scroll->ensureWidgetVisible(control); + control->setFocusFast(); + } +} + +void EditInformation::showError(EditField field) { + if (const auto control = controlForField(field)) { + _scroll->ensureWidgetVisible(control); + control->showError(QString()); + } +} + void EditInformation::setupControls() { const auto inner = setupContent(); @@ -175,7 +190,7 @@ not_null EditInformation::setupContent() { Type::Postcode, tr::lng_passport_postcode(tr::now), maxLabelWidth, - _information.shippingAddress.postCode, + _information.shippingAddress.postcode, QString(), kMaxPostcodeSize)); //StreetValidate, // #TODO payments @@ -230,6 +245,12 @@ void EditInformation::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } +void EditInformation::focusInEvent(QFocusEvent *e) { + if (const auto control = controlForField(_focusField)) { + control->setFocusFast(); + } +} + void EditInformation::updateControlsGeometry() { const auto submitTop = height() - _done->height(); _scroll->setGeometry(0, 0, width(), submitTop); @@ -243,6 +264,20 @@ void EditInformation::updateControlsGeometry() { _scroll->updateBars(); } +auto EditInformation::controlForField(EditField field) const -> Row* { + switch (field) { + case EditField::ShippingStreet: return _street1; + case EditField::ShippingCity: return _city; + case EditField::ShippingState: return _state; + case EditField::ShippingCountry: return _country; + case EditField::ShippingPostcode: return _postcode; + case EditField::Name: return _name; + case EditField::Email: return _email; + case EditField::Phone: return _phone; + } + Unexpected("Unknown field in EditInformation::controlForField."); +} + RequestedInformation EditInformation::collect() const { return { .name = _name ? _name->valueCurrent() : QString(), @@ -254,7 +289,7 @@ RequestedInformation EditInformation::collect() const { .city = _city ? _city->valueCurrent() : QString(), .state = _state ? _state->valueCurrent() : QString(), .countryIso2 = _country ? _country->valueCurrent() : QString(), - .postCode = _postcode ? _postcode->valueCurrent() : QString(), + .postcode = _postcode ? _postcode->valueCurrent() : QString(), }, }; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h index fecd70736..8fb434074 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -36,14 +36,19 @@ public: EditField field, not_null delegate); + void showError(EditField field); + void setFocus(EditField field); + private: using Row = Passport::Ui::PanelDetailsRow; void resizeEvent(QResizeEvent *e) override; + void focusInEvent(QFocusEvent *e) override; void setupControls(); [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); + [[nodiscard]] Row *controlForField(EditField field) const; [[nodiscard]] RequestedInformation collect() const; @@ -66,6 +71,8 @@ private: Row *_email = nullptr; Row *_phone = nullptr; + EditField _focusField = EditField::ShippingStreet; + }; } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 03656e6b2..41d44b4ee 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -44,7 +44,7 @@ FormSummary::FormSummary( tr::lng_payments_pay_amount( lt_amount, rpl::single(computeTotalAmount())), - st::passportPanelAuthorize) { + st::paymentsPanelSubmit) { setupControls(); } @@ -150,7 +150,7 @@ not_null FormSummary::setupContent() { push(_information.shippingAddress.city); push(_information.shippingAddress.state); push(_information.shippingAddress.countryIso2); - push(_information.shippingAddress.postCode); + push(_information.shippingAddress.postcode); info->updateContent( tr::lng_payments_shipping_address(tr::now), (list.isEmpty() ? "enter pls" : list.join(", ")), @@ -200,7 +200,7 @@ not_null FormSummary::setupContent() { const auto phone = inner->add(object_ptr(inner)); phone->addClickHandler([=] { _delegate->panelEditPhone(); }); phone->updateContent( - tr::lng_payments_info_email(tr::now), + tr::lng_payments_info_phone(tr::now), (_information.phone.isEmpty() ? "enter pls" : _information.phone), diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 22a977c87..63d9ba40b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -23,6 +23,7 @@ Panel::Panel(not_null delegate) , _widget(std::make_unique()) { _widget->setTitle(tr::lng_payments_checkout_title()); _widget->setInnerSize(st::passportPanelSize); + _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); _widget->closeRequests( ) | rpl::start_with_next([=] { @@ -52,18 +53,37 @@ void Panel::showForm( current, options, _delegate)); + _widget->setBackAllowed(false); } void Panel::showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, EditField field) { - _widget->showInner(base::make_unique_q( + auto edit = base::make_unique_q( _widget.get(), invoice, current, field, - _delegate)); + _delegate); + _weakEditWidget = edit.get(); + _widget->showInner(std::move(edit)); + _widget->setBackAllowed(true); + _weakEditWidget->setFocus(field); +} + +void Panel::showEditError( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field) { + if (_weakEditWidget) { + _weakEditWidget->showError(field); + } else { + showEditInformation(invoice, current, field); + if (_weakEditWidget && field == EditField::ShippingCountry) { + _weakEditWidget->showError(field); + } + } } void Panel::chooseShippingOption(const ShippingOptions &options) { @@ -89,6 +109,10 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { })); } +rpl::producer<> Panel::backRequests() const { + return _widget->backRequests(); +} + void Panel::showBox(object_ptr box) { _widget->showBox( std::move(box), @@ -96,8 +120,12 @@ void Panel::showBox(object_ptr box) { anim::type::normal); } -void Panel::showToast(const QString &text) { +void Panel::showToast(const TextWithEntities &text) { _widget->showToast(text); } +rpl::lifetime &Panel::lifetime() { + return _widget->lifetime(); +} + } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 102f0fa2c..9cd852731 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -23,6 +23,7 @@ struct Invoice; struct RequestedInformation; struct ShippingOptions; enum class EditField; +class EditInformation; class Panel final { public: @@ -39,14 +40,23 @@ public: const Invoice &invoice, const RequestedInformation ¤t, EditField field); + void showEditError( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field); void chooseShippingOption(const ShippingOptions &options); + [[nodiscard]] rpl::producer<> backRequests() const; + void showBox(object_ptr box); - void showToast(const QString &text); + void showToast(const TextWithEntities &text); + + [[nodiscard]] rpl::lifetime &lifetime(); private: const not_null _delegate; std::unique_ptr _widget; + QPointer _weakEditWidget; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index f282bcbc9..0ab3c0d23 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -53,7 +53,7 @@ struct Address { QString city; QString state; QString countryIso2; - QString postCode; + QString postcode; [[nodiscard]] bool valid() const { return !address1.isEmpty() @@ -70,7 +70,7 @@ struct Address { && (city == other.city) && (state == other.state) && (countryIso2 == other.countryIso2) - && (postCode == other.postCode); + && (postcode == other.postcode); } inline bool operator!=(const Address &other) const { return !(*this == other); @@ -117,7 +117,11 @@ struct SavedCredentials { }; enum class EditField { - ShippingInformation, + ShippingStreet, + ShippingCity, + ShippingState, + ShippingCountry, + ShippingPostcode, Name, Email, Phone, diff --git a/Telegram/SourceFiles/payments/ui/payments_webview.cpp b/Telegram/SourceFiles/payments/ui/payments_webview.cpp index 02ff0d193..39ae11b6c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_webview.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_webview.cpp @@ -21,6 +21,7 @@ using namespace ::Ui; class PanelDelegate; WebviewWindow::WebviewWindow( + const QString &userDataPath, const QString &url, not_null delegate) { if (!url.startsWith("https://", Qt::CaseInsensitive)) { @@ -49,9 +50,11 @@ WebviewWindow::WebviewWindow( QPainter(body).fillRect(clip, st::windowBg); }, body->lifetime()); + const auto path = _webview = Ui::CreateChild( window, - window); + window, + Webview::WindowConfig{ .userDataPath = userDataPath }); if (!_webview->widget()) { return; } diff --git a/Telegram/SourceFiles/payments/ui/payments_webview.h b/Telegram/SourceFiles/payments/ui/payments_webview.h index 738c77994..823d11775 100644 --- a/Telegram/SourceFiles/payments/ui/payments_webview.h +++ b/Telegram/SourceFiles/payments/ui/payments_webview.h @@ -22,6 +22,7 @@ class PanelDelegate; class WebviewWindow final { public: WebviewWindow( + const QString &userDataPath, const QString &url, not_null delegate); diff --git a/Telegram/SourceFiles/storage/storage_domain.cpp b/Telegram/SourceFiles/storage/storage_domain.cpp index b382a74a4..8528d4e43 100644 --- a/Telegram/SourceFiles/storage/storage_domain.cpp +++ b/Telegram/SourceFiles/storage/storage_domain.cpp @@ -273,4 +273,8 @@ void Domain::clearOldVersion() { _oldVersion = 0; } +QString Domain::webviewDataPath() const { + return BaseGlobalPath() + "webview"; +} + } // namespace Storage diff --git a/Telegram/SourceFiles/storage/storage_domain.h b/Telegram/SourceFiles/storage/storage_domain.h index beb9f1152..33e9ada85 100644 --- a/Telegram/SourceFiles/storage/storage_domain.h +++ b/Telegram/SourceFiles/storage/storage_domain.h @@ -44,6 +44,8 @@ public: [[nodiscard]] int oldVersion() const; void clearOldVersion(); + [[nodiscard]] QString webviewDataPath() const; + private: enum class StartModernResult { Success, diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp index 33b8139a2..a4652d746 100644 --- a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp @@ -87,19 +87,24 @@ private: }; CountrySelectBox::CountrySelectBox(QWidget*) -: _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { +: CountrySelectBox(nullptr, QString(), Type::Phones) { } CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) : _type(type) -, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { +, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) +, _ownedInner(this, type) { if (Data::CountriesByISO2().contains(iso)) { LastValidISO = iso; } } rpl::producer CountrySelectBox::countryChosen() const { - return _inner->countryChosen(); + Expects(_ownedInner != nullptr || _inner != nullptr); + + return (_ownedInner + ? _ownedInner.data() + : _inner.data())->countryChosen(); } void CountrySelectBox::prepare() { @@ -114,7 +119,7 @@ void CountrySelectBox::prepare() { }); _inner = setInnerWidget( - object_ptr(this, _type), + std::move(_ownedInner), st::countriesScroll, _select->height()); diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.h b/Telegram/SourceFiles/ui/boxes/country_select_box.h index ba580a342..6d90b75e5 100644 --- a/Telegram/SourceFiles/ui/boxes/country_select_box.h +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.h @@ -46,6 +46,7 @@ private: object_ptr _select; class Inner; + object_ptr _ownedInner; QPointer _inner; }; diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp index 84f8097b8..b05c98f93 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/labels.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/fade_wrap.h" -#include "ui/toast/toast.h" +#include "ui/toasts/common_toasts.h" #include "ui/widgets/tooltip.h" #include "ui/platform/ui_platform_utility.h" #include "ui/layers/layer_widget.h" @@ -266,8 +266,11 @@ void SeparatePanel::showBox( _layer->showBox(std::move(box), options, animated); } -void SeparatePanel::showToast(const QString &text) { - Ui::Toast::Show(this, text); +void SeparatePanel::showToast(const TextWithEntities &text) { + Ui::ShowMultilineToast({ + .parentOverride = this, + .text = text, + }); } void SeparatePanel::ensureLayerCreated() { diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.h b/Telegram/SourceFiles/ui/widgets/separate_panel.h index 95bfd5871..721674d29 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.h +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.h @@ -38,7 +38,7 @@ public: object_ptr box, Ui::LayerOptions options, anim::type animated); - void showToast(const QString &text); + void showToast(const TextWithEntities &text); void destroyLayer(); rpl::producer<> backRequests() const; From 994dbf9eb577bee3e470cebc037d1df9f63b4e4f Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 24 Mar 2021 16:41:46 +0400 Subject: [PATCH 014/127] Validate saved information on payment form open. --- .../payments/payments_checkout_process.cpp | 26 ++++++++++++++++++- .../payments/payments_checkout_process.h | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index ea7aff424..030803b83 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -101,8 +101,14 @@ not_null CheckoutProcess::panelDelegate() { void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { v::match(update.data, [&](const FormReady &) { - showForm(); + performInitialSilentValidation(); + if (!_initialSilentValidation) { + showForm(); + } }, [&](const ValidateFinished &) { + if (_initialSilentValidation) { + _initialSilentValidation = false; + } showForm(); if (_submitState == SubmitState::Validation) { _submitState = SubmitState::Validated; @@ -156,6 +162,11 @@ void CheckoutProcess::handleError(const Error &error) { if (_submitState == SubmitState::Validation) { _submitState = SubmitState::None; } + if (_initialSilentValidation) { + _initialSilentValidation = false; + showForm(); + return; + } if (id == u"REQ_INFO_NAME_INVALID"_q) { showEditError(Ui::EditField::Name); } else if (id == u"REQ_INFO_EMAIL_INVALID"_q) { @@ -373,6 +384,19 @@ void CheckoutProcess::panelShowBox(object_ptr box) { _panel->showBox(std::move(box)); } +void CheckoutProcess::performInitialSilentValidation() { + const auto &invoice = _form->invoice(); + const auto &saved = _form->savedInformation(); + if ((invoice.isNameRequested && saved.name.isEmpty()) + || (invoice.isEmailRequested && saved.email.isEmpty()) + || (invoice.isPhoneRequested && saved.phone.isEmpty()) + || (invoice.isShippingAddressRequested && !saved.shippingAddress)) { + return; + } + _initialSilentValidation = true; + _form->validateInformation(saved); +} + QString CheckoutProcess::webviewDataPath() const { return _session->domain().local().webviewDataPath(); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 65d09d986..bd3210d14 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -61,6 +61,7 @@ private: void showEditError(Ui::EditField field); void chooseShippingOption(); + void performInitialSilentValidation(); [[nodiscard]] QString webviewDataPath() const; void panelRequestClose() override; @@ -84,6 +85,7 @@ private: const std::unique_ptr _panel; std::unique_ptr _webviewWindow; SubmitState _submitState = SubmitState::None; + bool _initialSilentValidation = false; rpl::lifetime _lifetime; From 76b4e185189a88f3d62288ae05564d5598f5459d Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 24 Mar 2021 21:59:30 +0400 Subject: [PATCH 015/127] Port required parts of Stripe SDK to lib_stripe. --- Telegram/CMakeLists.txt | 2 + .../payments/stripe/stripe_address.h | 18 ++ .../payments/stripe/stripe_api_client.cpp | 167 ++++++++++++++++++ .../payments/stripe/stripe_api_client.h | 44 +++++ .../payments/stripe/stripe_callbacks.h | 19 ++ .../payments/stripe/stripe_card.cpp | 117 ++++++++++++ .../SourceFiles/payments/stripe/stripe_card.h | 80 +++++++++ .../payments/stripe/stripe_card_params.cpp | 33 ++++ .../payments/stripe/stripe_card_params.h | 33 ++++ .../payments/stripe/stripe_decode.cpp | 23 +++ .../payments/stripe/stripe_decode.h | 18 ++ .../payments/stripe/stripe_error.cpp | 83 +++++++++ .../payments/stripe/stripe_error.h | 65 +++++++ .../payments/stripe/stripe_form_encodable.h | 24 +++ .../payments/stripe/stripe_form_encoder.cpp | 40 +++++ .../payments/stripe/stripe_form_encoder.h | 21 +++ .../stripe/stripe_payment_configuration.h | 26 +++ .../SourceFiles/payments/stripe/stripe_pch.h | 13 ++ .../payments/stripe/stripe_token.cpp | 65 +++++++ .../payments/stripe/stripe_token.h | 49 +++++ Telegram/cmake/lib_stripe.cmake | 47 +++++ 21 files changed, 987 insertions(+) create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_address.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_api_client.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_callbacks.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card_params.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_decode.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_decode.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_error.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_error.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_pch.h create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_token.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_token.h create mode 100644 Telegram/cmake/lib_stripe.cmake diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index ec5aa69c8..9f5defbd5 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -27,6 +27,7 @@ get_filename_component(res_loc Resources REALPATH) include(cmake/telegram_options.cmake) include(cmake/lib_ffmpeg.cmake) +include(cmake/lib_stripe.cmake) include(cmake/lib_tgvoip.cmake) include(cmake/lib_tgcalls.cmake) include(cmake/td_export.cmake) @@ -58,6 +59,7 @@ PRIVATE desktop-app::lib_qr desktop-app::lib_webview desktop-app::lib_ffmpeg + desktop-app::lib_stripe desktop-app::external_lz4 desktop-app::external_rlottie desktop-app::external_zlib diff --git a/Telegram/SourceFiles/payments/stripe/stripe_address.h b/Telegram/SourceFiles/payments/stripe/stripe_address.h new file mode 100644 index 000000000..5be06f6cc --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_address.h @@ -0,0 +1,18 @@ +/* +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 Stripe { + +enum class BillingAddressFields { + None, + Zip, + Full, +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp b/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp new file mode 100644 index 000000000..5189fa506 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp @@ -0,0 +1,167 @@ +/* +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 "stripe/stripe_api_client.h" + +#include "stripe/stripe_error.h" +#include "stripe/stripe_token.h" +#include "stripe/stripe_form_encoder.h" + +#include +#include +#include +#include +#include + +namespace Stripe { +namespace { + +[[nodiscard]] QString APIURLBase() { + return "api.stripe.com/v1"; +} + +[[nodiscard]] QString TokenEndpoint() { + return "tokens"; +} + +[[nodiscard]] QString StripeAPIVersion() { + return "2015-10-12"; +} + +[[nodiscard]] QString SDKVersion() { + return "9.1.0"; +} + +[[nodiscard]] QString StripeUserAgentDetails() { + const auto details = QJsonObject{ + { "lang", "objective-c" }, + { "bindings_version", SDKVersion() }, + }; + return QString::fromUtf8( + QJsonDocument(details).toJson(QJsonDocument::Compact)); +} + +} // namespace + +APIClient::APIClient(PaymentConfiguration configuration) +: _apiUrl("https://" + APIURLBase()) +, _configuration(configuration) { + _additionalHttpHeaders = { + { "X-Stripe-User-Agent", StripeUserAgentDetails() }, + { "Stripe-Version", StripeAPIVersion() }, + { "Authorization", "Bearer " + _configuration.publishableKey }, + }; +} + +APIClient::~APIClient() { + const auto destroy = std::move(_old); +} + +void APIClient::createTokenWithCard( + CardParams card, + TokenCompletionCallback completion) { + createTokenWithData( + FormEncoder::formEncodedDataForObject(card), + std::move(completion)); +} + +void APIClient::createTokenWithData( + QByteArray data, + TokenCompletionCallback completion) { + const auto url = QUrl(_apiUrl + '/' + TokenEndpoint()); + auto request = QNetworkRequest(url); + for (const auto &[name, value] : _additionalHttpHeaders) { + request.setRawHeader(name.toUtf8(), value.toUtf8()); + } + destroyReplyDelayed(std::move(_reply)); + _reply.reset(_manager.post(request, data)); + const auto finish = [=](Token token, Error error) { + crl::on_main([ + completion, + token = std::move(token), + error = std::move(error) + ] { + completion(std::move(token), std::move(error)); + }); + }; + const auto finishWithError = [=](Error error) { + finish(Token::Empty(), std::move(error)); + }; + const auto finishWithToken = [=](Token token) { + finish(std::move(token), Error::None()); + }; + QObject::connect(_reply.get(), &QNetworkReply::finished, [=] { + const auto replyError = int(_reply->error()); + const auto replyErrorString = _reply->errorString(); + const auto bytes = _reply->readAll(); + destroyReplyDelayed(std::move(_reply)); + + auto parseError = QJsonParseError(); + const auto document = QJsonDocument::fromJson(bytes, &parseError); + if (!bytes.isEmpty()) { + if (parseError.error != QJsonParseError::NoError) { + const auto code = int(parseError.error); + finishWithError({ + Error::Code::JsonParse, + QString("InvalidJson%1").arg(code), + parseError.errorString(), + }); + return; + } else if (!document.isObject()) { + finishWithError({ + Error::Code::JsonFormat, + "InvalidJsonRoot", + "Not an object in JSON reply.", + }); + return; + } + const auto object = document.object(); + if (auto error = Error::DecodedObjectFromResponse(object)) { + finishWithError(std::move(error)); + return; + } + } + if (replyError != QNetworkReply::NoError) { + finishWithError({ + Error::Code::Network, + QString("RequestError%1").arg(replyError), + replyErrorString, + }); + return; + } + auto token = Token::DecodedObjectFromAPIResponse(document.object()); + if (!token) { + finishWithError({ + Error::Code::JsonFormat, + "InvalidTokenJson", + "Could not parse token.", + }); + } + finishWithToken(std::move(token)); + }); +} + +void APIClient::destroyReplyDelayed(std::unique_ptr reply) { + if (!reply) { + return; + } + const auto raw = reply.get(); + _old.push_back(std::move(reply)); + QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr); + raw->deleteLater(); + QObject::connect(raw, &QObject::destroyed, [=] { + for (auto i = begin(_old); i != end(_old); ++i) { + if (i->get() == raw) { + i->release(); + _old.erase(i); + break; + } + } + }); +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_api_client.h b/Telegram/SourceFiles/payments/stripe/stripe_api_client.h new file mode 100644 index 000000000..020df992d --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_api_client.h @@ -0,0 +1,44 @@ +/* +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 "stripe/stripe_payment_configuration.h" +#include "stripe/stripe_card_params.h" +#include "stripe/stripe_callbacks.h" + +#include +#include +#include + +namespace Stripe { + +class APIClient final { +public: + explicit APIClient(PaymentConfiguration configuration); + ~APIClient(); + + void createTokenWithCard( + CardParams card, + TokenCompletionCallback completion); + void createTokenWithData( + QByteArray data, + TokenCompletionCallback completion); + +private: + void destroyReplyDelayed(std::unique_ptr reply); + + QString _apiUrl; + PaymentConfiguration _configuration; + std::map _additionalHttpHeaders; + QNetworkAccessManager _manager; + std::unique_ptr _reply; + std::vector> _old; + +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_callbacks.h b/Telegram/SourceFiles/payments/stripe/stripe_callbacks.h new file mode 100644 index 000000000..8e0d0a2a5 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_callbacks.h @@ -0,0 +1,19 @@ +/* +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 + +namespace Stripe { + +class Error; +class Token; + +using TokenCompletionCallback = std::function; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card.cpp new file mode 100644 index 000000000..af37edb9c --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card.cpp @@ -0,0 +1,117 @@ +/* +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 "stripe/stripe_card.h" + +#include "stripe/stripe_decode.h" + +namespace Stripe { + +Card::Card( + QString id, + QString last4, + CardBrand brand, + quint32 expMonth, + quint32 expYear) +: _cardId(id) +, _last4(last4) +, _brand(brand) +, _expMonth(expMonth) +, _expYear(expYear) { +} + +Card Card::Empty() { + return Card(QString(), QString(), CardBrand::Unknown, 0, 0); +} + +[[nodiscard]] CardBrand BrandFromString(const QString &brand) { + if (brand == "visa") { + return CardBrand::Visa; + } else if (brand == "american express") { + return CardBrand::Amex; + } else if (brand == "mastercard") { + return CardBrand::MasterCard; + } else if (brand == "discover") { + return CardBrand::Discover; + } else if (brand == "jcb") { + return CardBrand::JCB; + } else if (brand == "diners club") { + return CardBrand::DinersClub; + } else { + return CardBrand::Unknown; + } +} + +[[nodiscard]] CardFundingType FundingFromString(const QString &funding) { + if (funding == "credit") { + return CardFundingType::Credit; + } else if (funding == "debit") { + return CardFundingType::Debit; + } else if (funding == "prepaid") { + return CardFundingType::Prepaid; + } else { + return CardFundingType::Other; + } +} + +Card Card::DecodedObjectFromAPIResponse(QJsonObject object) { + if (!ContainsFields(object, { + u"id", + u"last4", + u"brand", + u"exp_month", + u"exp_year" + })) { + return Card::Empty(); + } + + const auto string = [&](QStringView key) { + return object.value(key).toString(); + }; + const auto cardId = string(u"id"); + const auto last4 = string(u"last4"); + const auto brand = BrandFromString(string(u"brand").toLower()); + const auto expMonth = object.value("exp_month").toInt(); + const auto expYear = object.value("exp_year").toInt(); + auto result = Card(cardId, last4, brand, expMonth, expYear); + result._name = string(u"name"); + result._dynamicLast4 = string(u"dynamic_last4"); + result._funding = FundingFromString(string(u"funding")); + result._fingerprint = string(u"fingerprint"); + result._country = string(u"country"); + result._currency = string(u"currency"); + result._addressLine1 = string(u"address_line1"); + result._addressLine2 = string(u"address_line2"); + result._addressCity = string(u"address_city"); + result._addressState = string(u"address_state"); + result._addressZip = string(u"address_zip"); + result._addressCountry = string(u"address_country"); + + // TODO incomplete, not used. + //result._allResponseFields = object; + + return result; +} + +bool Card::empty() const { + return _cardId.isEmpty(); +} + +QString CardBrandToString(CardBrand brand) { + switch (brand) { + case CardBrand::Amex: return "American Express"; + case CardBrand::DinersClub: return "Diners Club"; + case CardBrand::Discover: return "Discover"; + case CardBrand::JCB: return "JCB"; + case CardBrand::MasterCard: return "MasterCard"; + case CardBrand::Unknown: return "Unknown"; + case CardBrand::Visa: return "Visa"; + } + std::abort(); +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card.h b/Telegram/SourceFiles/payments/stripe/stripe_card.h new file mode 100644 index 000000000..d7cb63627 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card.h @@ -0,0 +1,80 @@ +/* +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 + +class QJsonObject; + +namespace Stripe { + +enum class CardBrand { + Visa, + Amex, + MasterCard, + Discover, + JCB, + DinersClub, + Unknown, +}; + +enum class CardFundingType { + Debit, + Credit, + Prepaid, + Other, +}; + +class Card final { +public: + Card(const Card &other) = default; + Card &operator=(const Card &other) = default; + Card(Card &&other) = default; + Card &operator=(Card &&other) = default; + ~Card() = default; + + [[nodiscard]] static Card Empty(); + [[nodiscard]] static Card DecodedObjectFromAPIResponse( + QJsonObject object); + + [[nodiscard]] bool empty() const; + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + +private: + Card( + QString id, + QString last4, + CardBrand brand, + quint32 expMonth, + quint32 expYear); + + QString _cardId; + QString _name; + QString _last4; + QString _dynamicLast4; + CardBrand _brand = CardBrand::Unknown; + CardFundingType _funding = CardFundingType::Other; + QString _fingerprint; + QString _country; + QString _currency; + quint32 _expMonth = 0; + quint32 _expYear = 0; + QString _addressLine1; + QString _addressLine2; + QString _addressCity; + QString _addressState; + QString _addressZip; + QString _addressCountry; + +}; + +[[nodiscard]] QString CardBrandToString(CardBrand brand); + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp new file mode 100644 index 000000000..06894fe87 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp @@ -0,0 +1,33 @@ +/* +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 "stripe/stripe_card_params.h" + +namespace Stripe { + +QString CardParams::RootObjectName() const { + return "card"; +} + +std::map CardParams::formFieldValues() const { + return { + { "number", number }, + { "cvc", cvc }, + { "name", name }, + { "address_line1", addressLine1 }, + { "address_line2", addressLine2 }, + { "address_city", addressCity }, + { "address_state", addressState }, + { "address_zip", addressZip }, + { "address_country", addressCountry }, + { "exp_month", QString::number(expMonth) }, + { "exp_year", QString::number(expYear) }, + { "currency", currency }, + }; +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_params.h b/Telegram/SourceFiles/payments/stripe/stripe_card_params.h new file mode 100644 index 000000000..811ab67a8 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_params.h @@ -0,0 +1,33 @@ +/* +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 "stripe/stripe_form_encodable.h" + +namespace Stripe { + +class CardParams final : public FormEncodable { +public: + QString RootObjectName() const override; + std::map formFieldValues() const override; + + QString number; + quint32 expMonth = 0; + quint32 expYear = 0; + QString cvc; + QString name; + QString addressLine1; + QString addressLine2; + QString addressCity; + QString addressState; + QString addressZip; + QString addressCountry; + QString currency; +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_decode.cpp b/Telegram/SourceFiles/payments/stripe/stripe_decode.cpp new file mode 100644 index 000000000..acd2dac0b --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_decode.cpp @@ -0,0 +1,23 @@ +/* +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 "stripe/stripe_decode.h" + +namespace Stripe { + +[[nodiscard]] bool ContainsFields( + const QJsonObject &object, + std::vector keys) { + for (const auto &key : keys) { + if (object.value(key).isUndefined()) { + return false; + } + } + return true; +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_decode.h b/Telegram/SourceFiles/payments/stripe/stripe_decode.h new file mode 100644 index 000000000..41edb3445 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_decode.h @@ -0,0 +1,18 @@ +/* +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 + +namespace Stripe { + +[[nodiscard]] bool ContainsFields( + const QJsonObject &object, + std::vector keys); + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_error.cpp b/Telegram/SourceFiles/payments/stripe/stripe_error.cpp new file mode 100644 index 000000000..675d41d8f --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_error.cpp @@ -0,0 +1,83 @@ +/* +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 "stripe/stripe_error.h" + +#include "stripe/stripe_decode.h" + +namespace Stripe { + +Error Error::DecodedObjectFromResponse(QJsonObject object) { + const auto entry = object.value("error"); + if (!entry.isObject()) { + return Error::None(); + } + const auto error = entry.toObject(); + const auto string = [&](QStringView key) { + return error.value(key).toString(); + }; + const auto type = string(u"type"); + const auto message = string(u"message"); + const auto parameterSnakeCase = string(u"param"); + + // There should always be a message and type for the error + if (message.isEmpty() || type.isEmpty()) { + return { + Code::API, + "GenericError", + "Could not interpret the error response " + "that was returned from Stripe." + }; + } + + auto parameterWords = parameterSnakeCase.isEmpty() + ? QStringList() + : parameterSnakeCase.split('_', Qt::SkipEmptyParts); + auto first = true; + for (auto &word : parameterWords) { + if (first) { + first = false; + } else { + word = word[0].toUpper() + word.midRef(1); + } + } + const auto parameter = parameterWords.join(QString()); + if (type == "api_error") { + return { Code::API, "GenericError", message, parameter }; + } else if (type == "invalid_request_error") { + return { Code::InvalidRequest, "GenericError", message, parameter }; + } else if (type != "card_error") { + return { Code::Unknown, type, message, parameter }; + } + const auto code = string(u"code"); + const auto cardError = [&](const QString &description) { + return Error{ Code::Card, description, message, parameter }; + }; + if (code == "incorrect_number") { + return cardError("IncorrectNumber"); + } else if (code == "invalid_number") { + return cardError("InvalidNumber"); + } else if (code == "invalid_expiry_month") { + return cardError("InvalidExpiryMonth"); + } else if (code == "invalid_expiry_year") { + return cardError("InvalidExpiryYear"); + } else if (code == "invalid_cvc") { + return cardError("InvalidCVC"); + } else if (code == "expired_card") { + return cardError("ExpiredCard"); + } else if (code == "incorrect_cvc") { + return cardError("IncorrectCVC"); + } else if (code == "card_declined") { + return cardError("CardDeclined"); + } else if (code == "processing_error") { + return cardError("ProcessingError"); + } else { + return cardError(code); + } +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_error.h b/Telegram/SourceFiles/payments/stripe/stripe_error.h new file mode 100644 index 000000000..9a0a46ec3 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_error.h @@ -0,0 +1,65 @@ +/* +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 + +class QJsonObject; + +namespace Stripe { + +class Error { +public: + enum class Code { + None = 0, // Non-Stripe errors. + JsonParse = -1, + JsonFormat = -2, + Network = -3, + + Unknown = 8, + Connection = 40, // Trouble connecting to Stripe. + InvalidRequest = 50, // Your request had invalid parameters. + API = 60, // General-purpose API error (should be rare). + Card = 70, // Something was wrong with the given card (most common). + Cancellation = 80, // The operation was cancelled. + CheckoutUnknown = 5000, // Checkout failed + CheckoutTooManyAttempts = 5001, // Too many incorrect code attempts + }; + + Error( + Code code, + const QString &description, + const QString &message, + const QString ¶meter = QString()) + : _code(code) + , _description(description) + , _message(message) + , _parameter(parameter) { + } + + [[nodiscard]] static Error None() { + return Error(Code::None, QString(), QString()); + } + [[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object); + + [[nodiscard]] bool empty() const { + return (_code == Code::None); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + +private: + Code _code = Code::None; + QString _description; + QString _message; + QString _parameter; + +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h b/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h new file mode 100644 index 000000000..1a09d69b1 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.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 + +#include +#include + +namespace Stripe { + +class FormEncodable { +public: + [[nodiscard]] virtual QString RootObjectName() const = 0; + + // TODO incomplete, not used: nested complex structures not supported. + [[nodiscard]] virtual std::map formFieldValues() const + = 0; +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp new file mode 100644 index 000000000..8ce752108 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp @@ -0,0 +1,40 @@ +/* +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 "stripe/stripe_form_encoder.h" + +#include +#include + +namespace Stripe { + +QByteArray FormEncoder::formEncodedDataForObject( + FormEncodable &object) { + const auto root = object.RootObjectName(); + const auto values = object.formFieldValues(); + auto result = QByteArray(); + auto keys = std::vector(); + for (const auto &[key, value] : values) { + if (!value.isEmpty()) { + keys.push_back(key); + } + } + std::sort(begin(keys), end(keys)); + const auto encode = [](const QString &value) { + return QUrl::toPercentEncoding(value); + }; + for (const auto &key : keys) { + const auto fullKey = root.isEmpty() ? key : (root + '[' + key + ']'); + if (!result.isEmpty()) { + result += '&'; + } + result += encode(fullKey) + '=' + encode(values.at(key)); + } + return result; +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h new file mode 100644 index 000000000..798de928c --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h @@ -0,0 +1,21 @@ +/* +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 "stripe/stripe_form_encodable.h" + +namespace Stripe { + +class FormEncoder { +public: + [[nodiscard]] static QByteArray formEncodedDataForObject( + FormEncodable &object); + +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h b/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h new file mode 100644 index 000000000..f2b2867ae --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h @@ -0,0 +1,26 @@ +/* +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 "stripe/stripe_address.h" + +#include + +namespace Stripe { + +struct PaymentConfiguration { + QString publishableKey; + // PaymentMethodType additionalPaymentMethods; // Apply Pay + BillingAddressFields requiredBillingAddressFields + = BillingAddressFields::None; + QString companyName; + // QString appleMerchantIdentifier; // Apple Pay + // bool smsAutofillDisabled = true; // Mobile only +}; + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_pch.h b/Telegram/SourceFiles/payments/stripe/stripe_pch.h new file mode 100644 index 000000000..b7c2e9fee --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_pch.h @@ -0,0 +1,13 @@ +/* +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 +#include +#include +#include +#include diff --git a/Telegram/SourceFiles/payments/stripe/stripe_token.cpp b/Telegram/SourceFiles/payments/stripe/stripe_token.cpp new file mode 100644 index 000000000..66a131a36 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_token.cpp @@ -0,0 +1,65 @@ +/* +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 "stripe/stripe_token.h" + +#include "stripe/stripe_decode.h" + +namespace Stripe { + +QString Token::tokenId() const { + return _tokenId; +} + +bool Token::livemode() const { + return _livemode; +} + +Card Token::card() const { + return _card; +} + +Token Token::Empty() { + return Token(QString(), false, QDateTime()); +} + +Token Token::DecodedObjectFromAPIResponse(QJsonObject object) { + if (!ContainsFields(object, { u"id", u"livemode", u"created" })) { + return Token::Empty(); + } + const auto tokenId = object.value("id").toString(); + const auto livemode = object.value("livemode").toBool(); + const auto created = QDateTime::fromTime_t( + object.value("created").toDouble()); + auto result = Token(tokenId, livemode, created); + const auto card = object.value("card"); + if (card.isObject()) { + result._card = Card::DecodedObjectFromAPIResponse(card.toObject()); + } + + // TODO incomplete, not used. + //const auto bankAccount = object.value("bank_account"); + //if (bankAccount.isObject()) { + // result._bankAccount = bankAccount::DecodedObjectFromAPIResponse( + // bankAccount.toObject()); + //} + //result._allResponseFields = object; + + return result; +} + +bool Token::empty() const { + return _tokenId.isEmpty(); +} + +Token::Token(QString tokenId, bool livemode, QDateTime created) +: _tokenId(std::move(tokenId)) +, _livemode(livemode) +, _created(std::move(created)) { +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_token.h b/Telegram/SourceFiles/payments/stripe/stripe_token.h new file mode 100644 index 000000000..f22aefe60 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_token.h @@ -0,0 +1,49 @@ +/* +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 "stripe/stripe_card.h" + +#include + +class QJsonObject; + +namespace Stripe { + +class Token { +public: + Token(const Token &other) = default; + Token &operator=(const Token &other) = default; + Token(Token &&other) = default; + Token &operator=(Token &&other) = default; + ~Token() = default; + + [[nodiscard]] QString tokenId() const; + [[nodiscard]] bool livemode() const; + [[nodiscard]] Card card() const; + + [[nodiscard]] static Token Empty(); + [[nodiscard]] static Token DecodedObjectFromAPIResponse( + QJsonObject object); + + [[nodiscard]] bool empty() const; + [[nodiscard]] explicit operator bool() const { + return !empty(); + } + +private: + Token(QString tokenId, bool livemode, QDateTime created); + + QString _tokenId; + bool _livemode = false; + QDateTime _created; + Card _card = Card::Empty(); + +}; + +} // namespace Stripe diff --git a/Telegram/cmake/lib_stripe.cmake b/Telegram/cmake/lib_stripe.cmake new file mode 100644 index 000000000..6785d983b --- /dev/null +++ b/Telegram/cmake/lib_stripe.cmake @@ -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 + +add_library(lib_stripe OBJECT) +add_library(desktop-app::lib_stripe ALIAS lib_stripe) +init_target(lib_stripe) + +set(stripe_src_loc ${src_loc}/payments) + +target_precompile_headers(lib_stripe PRIVATE ${stripe_src_loc}/stripe/stripe_pch.h) +nice_target_sources(lib_stripe ${stripe_src_loc} +PRIVATE + stripe/stripe_address.h + stripe/stripe_api_client.cpp + stripe/stripe_api_client.h + stripe/stripe_callbacks.h + stripe/stripe_card.cpp + stripe/stripe_card.h + stripe/stripe_card_params.cpp + stripe/stripe_card_params.h + stripe/stripe_decode.cpp + stripe/stripe_decode.h + stripe/stripe_error.cpp + stripe/stripe_error.h + stripe/stripe_form_encodable.h + stripe/stripe_form_encoder.cpp + stripe/stripe_form_encoder.h + stripe/stripe_payment_configuration.h + stripe/stripe_token.cpp + stripe/stripe_token.h + + stripe/stripe_pch.h +) + +target_include_directories(lib_stripe +PUBLIC + ${stripe_src_loc} +) + +target_link_libraries(lib_stripe +PUBLIC + desktop-app::lib_crl + desktop-app::external_qt +) From 5bc6e6533fd6cc6932d461e258f8b6e93406fd24 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 Mar 2021 19:10:01 +0400 Subject: [PATCH 016/127] Fix jumping of Media Viewer in some DEs. --- Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index d69a4e430..fd5f6a34f 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -486,7 +486,6 @@ void OverlayWidget::moveEvent(QMoveEvent *e) { DEBUG_LOG(("Viewer Pos: Moved to %1, %2") .arg(newPos.x()) .arg(newPos.y())); - moveToScreen(); OverlayParent::moveEvent(e); } From 5e4bc200c22ed3eaac2cb68dd8f3932ff41ca81f Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 Mar 2021 19:27:30 +0400 Subject: [PATCH 017/127] Support entering card details natively. --- Telegram/Resources/langs/lang.strings | 20 +- .../payments/payments_checkout_process.cpp | 119 +++++++-- .../payments/payments_checkout_process.h | 14 +- .../SourceFiles/payments/payments_form.cpp | 74 +++++- Telegram/SourceFiles/payments/payments_form.h | 53 +++- .../payments/stripe/stripe_api_client.cpp | 2 +- .../payments/stripe/stripe_card.cpp | 111 ++++++-- .../SourceFiles/payments/stripe/stripe_card.h | 18 ++ .../payments/stripe/stripe_card_params.cpp | 2 +- .../payments/stripe/stripe_card_params.h | 9 +- .../payments/stripe/stripe_form_encodable.h | 23 +- .../payments/stripe/stripe_form_encoder.cpp | 4 +- .../payments/stripe/stripe_form_encoder.h | 2 +- .../stripe/stripe_payment_configuration.h | 7 +- .../payments/ui/payments_edit_card.cpp | 245 ++++++++++++++++++ .../payments/ui/payments_edit_card.h | 73 ++++++ .../payments/ui/payments_edit_information.cpp | 24 +- .../payments/ui/payments_edit_information.h | 10 +- .../payments/ui/payments_form_summary.cpp | 28 +- .../payments/ui/payments_form_summary.h | 2 + .../payments/ui/payments_panel.cpp | 73 +++++- .../SourceFiles/payments/ui/payments_panel.h | 22 +- .../payments/ui/payments_panel_data.h | 42 ++- .../payments/ui/payments_panel_delegate.h | 3 + Telegram/cmake/td_ui.cmake | 2 + 25 files changed, 859 insertions(+), 123 deletions(-) create mode 100644 Telegram/SourceFiles/payments/ui/payments_edit_card.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_edit_card.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0a2a9c6ae..287c713a3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1863,20 +1863,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_payment_method" = "Payment Method"; +"lng_payments_payment_method_ph" = "Enter your card details"; "lng_payments_shipping_address" = "Shipping Information"; +"lng_payments_shipping_address_ph" = "Enter your shipping information"; "lng_payments_shipping_method" = "Shipping Method"; +"lng_payments_shipping_method_ph" = "Choose your shipping method"; "lng_payments_info_name" = "Name"; +"lng_payments_info_name_ph" = "Enter your name"; "lng_payments_info_email" = "Email"; +"lng_payments_info_email_ph" = "Enter your email"; "lng_payments_info_phone" = "Phone"; +"lng_payments_info_phone_ph" = "Enter your phone number"; "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_payment_card" = "Payment Card"; +"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..."; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 030803b83..4684896f6 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -18,6 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "core/local_url_handlers.h" // TryConvertUrlToLocal. #include "apiwrap.h" +#include "stripe/stripe_api_client.h" +#include "stripe/stripe_error.h" +#include "stripe/stripe_token.h" // #TODO payments errors #include "mainwindow.h" @@ -54,6 +57,13 @@ base::flat_map, SessionProcesses> Processes; return result; } +[[nodiscard]] QString CardTitle(const Stripe::Card &card) { + // Like server stores saved_credentials title. + return Stripe::CardBrandToString(card.brand()).toLower() + + " *" + + card.last4(); +} + } // namespace void CheckoutProcess::Start(not_null item) { @@ -167,22 +177,23 @@ void CheckoutProcess::handleError(const Error &error) { showForm(); return; } + using Field = Ui::InformationField; if (id == u"REQ_INFO_NAME_INVALID"_q) { - showEditError(Ui::EditField::Name); + showInformationError(Field::Name); } else if (id == u"REQ_INFO_EMAIL_INVALID"_q) { - showEditError(Ui::EditField::Email); + showInformationError(Field::Email); } else if (id == u"REQ_INFO_PHONE_INVALID"_q) { - showEditError(Ui::EditField::Phone); + showInformationError(Field::Phone); } else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) { - showEditError(Ui::EditField::ShippingStreet); + showInformationError(Field::ShippingStreet); } else if (id == u"ADDRESS_CITY_INVALID"_q) { - showEditError(Ui::EditField::ShippingCity); + showInformationError(Field::ShippingCity); } else if (id == u"ADDRESS_STATE_INVALID"_q) { - showEditError(Ui::EditField::ShippingState); + showInformationError(Field::ShippingState); } else if (id == u"ADDRESS_COUNTRY_INVALID"_q) { - showEditError(Ui::EditField::ShippingCountry); + showInformationError(Field::ShippingCountry); } else if (id == u"ADDRESS_POSTCODE_INVALID"_q) { - showEditError(Ui::EditField::ShippingPostcode); + showInformationError(Field::ShippingPostcode); } else if (id == u"SHIPPING_BOT_TIMEOUT"_q) { showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message } else if (id == u"SHIPPING_NOT_AVAILABLE"_q) { @@ -238,6 +249,7 @@ void CheckoutProcess::panelSubmit() { || _submitState == SubmitState::Finishing) { return; } + const auto &native = _form->nativePayment(); const auto &invoice = _form->invoice(); const auto &options = _form->shippingOptions(); if (!options.list.empty() && options.selectedId.isEmpty()) { @@ -252,14 +264,23 @@ void CheckoutProcess::panelSubmit() { _submitState = SubmitState::Validation; _form->validateInformation(_form->savedInformation()); return; + } else if (native + && !native.newCredentials + && !native.savedCredentials) { + editPaymentMethod(); + return; } _submitState = SubmitState::Finishing; - _webviewWindow = std::make_unique( - webviewDataPath(), - _form->details().url, - panelDelegate()); - if (!_webviewWindow->shown()) { - // #TODO payments errors + if (!native) { + _webviewWindow = std::make_unique( + webviewDataPath(), + _form->details().url, + panelDelegate()); + if (!_webviewWindow->shown()) { + // #TODO payments errors + } + } else if (native.newCredentials) { + _form->send(native.newCredentials.data); } } @@ -316,30 +337,82 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { return false; } +void CheckoutProcess::panelEditPaymentMethod() { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { + return; + } + editPaymentMethod(); +} + +void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) { + Expects(_form->nativePayment().type == NativePayment::Type::Stripe); + Expects(!_form->nativePayment().stripePublishableKey.isEmpty()); + + if (_stripe) { + return; + } + auto configuration = Stripe::PaymentConfiguration{ + .publishableKey = _form->nativePayment().stripePublishableKey, + .companyName = "Telegram", + }; + _stripe = std::make_unique(std::move(configuration)); + auto card = Stripe::CardParams{ + .number = data.number, + .expMonth = data.expireMonth, + .expYear = data.expireYear, + .cvc = data.cvc, + .name = data.cardholderName, + .addressZip = data.addressZip, + .addressCountry = data.addressCountry, + }; + _stripe->createTokenWithCard(std::move(card), crl::guard(this, [=]( + Stripe::Token token, + Stripe::Error error) { + _stripe = nullptr; + + if (error) { + int a = 0; + // #TODO payment errors + } else { + _form->setPaymentCredentials({ + .title = CardTitle(token.card()), + .data = QJsonDocument(QJsonObject{ + { "type", "card" }, + { "id", token.tokenId() }, + }).toJson(QJsonDocument::Compact), + .saveOnServer = false, + }); + showForm(); + } + })); +} + void CheckoutProcess::panelEditShippingInformation() { - showEditInformation(Ui::EditField::ShippingStreet); + showEditInformation(Ui::InformationField::ShippingStreet); } void CheckoutProcess::panelEditName() { - showEditInformation(Ui::EditField::Name); + showEditInformation(Ui::InformationField::Name); } void CheckoutProcess::panelEditEmail() { - showEditInformation(Ui::EditField::Email); + showEditInformation(Ui::InformationField::Email); } void CheckoutProcess::panelEditPhone() { - showEditInformation(Ui::EditField::Phone); + showEditInformation(Ui::InformationField::Phone); } void CheckoutProcess::showForm() { _panel->showForm( _form->invoice(), _form->savedInformation(), + _form->nativePayment().details, _form->shippingOptions()); } -void CheckoutProcess::showEditInformation(Ui::EditField field) { +void CheckoutProcess::showEditInformation(Ui::InformationField field) { if (_submitState != SubmitState::None) { return; } @@ -349,11 +422,11 @@ void CheckoutProcess::showEditInformation(Ui::EditField field) { field); } -void CheckoutProcess::showEditError(Ui::EditField field) { +void CheckoutProcess::showInformationError(Ui::InformationField field) { if (_submitState != SubmitState::None) { return; } - _panel->showEditError( + _panel->showInformationError( _form->invoice(), _form->savedInformation(), field); @@ -363,6 +436,10 @@ void CheckoutProcess::chooseShippingOption() { _panel->chooseShippingOption(_form->shippingOptions()); } +void CheckoutProcess::editPaymentMethod() { + _panel->choosePaymentMethod(_form->nativePayment().details); +} + void CheckoutProcess::panelChooseShippingOption() { if (_submitState != SubmitState::None) { return; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index bd3210d14..3613ff232 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HistoryItem; +namespace Stripe { +class APIClient; +} // namespace Stripe + namespace Main { class Session; } // namespace Main @@ -19,7 +23,7 @@ class Session; namespace Payments::Ui { class Panel; class WebviewWindow; -enum class EditField; +enum class InformationField; } // namespace Payments::Ui namespace Payments { @@ -57,9 +61,10 @@ private: void handleError(const Error &error); void showForm(); - void showEditInformation(Ui::EditField field); - void showEditError(Ui::EditField field); + void showEditInformation(Ui::InformationField field); + void showInformationError(Ui::InformationField field); void chooseShippingOption(); + void editPaymentMethod(); void performInitialSilentValidation(); [[nodiscard]] QString webviewDataPath() const; @@ -70,6 +75,7 @@ private: void panelWebviewMessage(const QJsonDocument &message) override; bool panelWebviewNavigationAttempt(const QString &uri) override; + void panelEditPaymentMethod() override; void panelEditShippingInformation() override; void panelEditName() override; void panelEditEmail() override; @@ -78,12 +84,14 @@ private: void panelChangeShippingOption(const QString &id) override; void panelValidateInformation(Ui::RequestedInformation data) override; + void panelValidateCard(Ui::UncheckedCardDetails data) override; void panelShowBox(object_ptr box) override; const not_null _session; const std::unique_ptr _form; const std::unique_ptr _panel; std::unique_ptr _webviewWindow; + std::unique_ptr _stripe; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 3e1a06869..62f9b59cb 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "apiwrap.h" +#include +#include +#include + namespace Payments { namespace { @@ -101,7 +105,7 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { processSavedCredentials(data); }); } - + fillNativePaymentInformation(); _updates.fire({ FormReady{} }); } @@ -152,10 +156,65 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { void Form::processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data) { - _savedCredentials = Ui::SavedCredentials{ - .id = qs(data.vid()), - .title = qs(data.vtitle()), + // #TODO payments not yet supported + //_nativePayment.savedCredentials = SavedCredentials{ + // .id = qs(data.vid()), + // .title = qs(data.vtitle()), + //}; + refreshNativePaymentDetails(); +} + +void Form::refreshNativePaymentDetails() { + const auto &saved = _nativePayment.savedCredentials; + const auto &entered = _nativePayment.newCredentials; + _nativePayment.details.credentialsTitle = entered + ? entered.title + : saved.title; + _nativePayment.details.ready = entered || saved; +} + +void Form::fillNativePaymentInformation() { + auto saved = std::move(_nativePayment.savedCredentials); + auto entered = std::move(_nativePayment.newCredentials); + _nativePayment = NativePayment(); + if (_details.nativeProvider != "stripe") { + return; + } + auto error = QJsonParseError(); + auto document = QJsonDocument::fromJson( + _details.nativeParamsJson, + &error); + if (error.error != QJsonParseError::NoError) { + LOG(("Payment Error: Could not decode native_params, error %1: %2" + ).arg(error.error + ).arg(error.errorString())); + return; + } else if (!document.isObject()) { + LOG(("Payment Error: Not an object in native_params.")); + return; + } + const auto object = document.object(); + const auto value = [&](QStringView key) { + return object.value(key); }; + const auto key = value(u"publishable_key").toString(); + if (key.isEmpty()) { + LOG(("Payment Error: No publishable_key in native_params.")); + return; + } + _nativePayment = NativePayment{ + .type = NativePayment::Type::Stripe, + .stripePublishableKey = key, + .savedCredentials = std::move(saved), + .newCredentials = std::move(entered), + .details = Ui::NativePaymentDetails{ + .supported = true, + .needCountry = value(u"need_country").toBool(), + .needZip = value(u"need_zip").toBool(), + .needCardholderName = value(u"need_cardholder_name").toBool(), + }, + }; + refreshNativePaymentDetails(); } void Form::send(const QByteArray &serializedCredentials) { @@ -221,6 +280,13 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { }).send(); } +void Form::setPaymentCredentials(const NewCredentials &credentials) { + Expects(!credentials.empty()); + + _nativePayment.newCredentials = credentials; + refreshNativePaymentDetails(); +} + void Form::setShippingOption(const QString &id) { _shippingOptions.selectedId = id; } diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 565340654..fbe4bd44f 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -33,6 +33,50 @@ struct FormDetails { } }; +struct SavedCredentials { + QString id; + QString title; + + [[nodiscard]] bool valid() const { + return !id.isEmpty(); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + +struct NewCredentials { + QString title; + QByteArray data; + bool saveOnServer = false; + + [[nodiscard]] bool empty() const { + return data.isEmpty(); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } +}; + +struct NativePayment { + enum class Type { + None, + Stripe, + }; + Type type = Type::None; + QString stripePublishableKey; + SavedCredentials savedCredentials; + NewCredentials newCredentials; + Ui::NativePaymentDetails details; + + [[nodiscard]] bool valid() const { + return (type != Type::None); + } + [[nodiscard]] explicit operator bool() const { + return valid(); + } +}; + struct FormReady {}; struct ValidateFinished {}; struct Error { @@ -73,8 +117,8 @@ public: [[nodiscard]] const Ui::RequestedInformation &savedInformation() const { return _savedInformation; } - [[nodiscard]] const Ui::SavedCredentials &savedCredentials() const { - return _savedCredentials; + [[nodiscard]] const NativePayment &nativePayment() const { + return _nativePayment; } [[nodiscard]] const Ui::ShippingOptions &shippingOptions() const { return _shippingOptions; @@ -85,6 +129,7 @@ public: } void validateInformation(const Ui::RequestedInformation &information); + void setPaymentCredentials(const NewCredentials &credentials); void setShippingOption(const QString &id); void send(const QByteArray &serializedCredentials); @@ -97,6 +142,8 @@ private: void processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data); void processShippingOptions(const QVector &data); + void fillNativePaymentInformation(); + void refreshNativePaymentDetails(); const not_null _session; MTP::Sender _api; @@ -105,7 +152,7 @@ private: Ui::Invoice _invoice; FormDetails _details; Ui::RequestedInformation _savedInformation; - Ui::SavedCredentials _savedCredentials; + NativePayment _nativePayment; Ui::RequestedInformation _validatedInformation; mtpRequestId _validateRequestId = 0; diff --git a/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp b/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp index 5189fa506..798741c96 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp @@ -65,7 +65,7 @@ void APIClient::createTokenWithCard( CardParams card, TokenCompletionCallback completion) { createTokenWithData( - FormEncoder::formEncodedDataForObject(card), + FormEncoder::formEncodedDataForObject(MakeEncodable(card)), std::move(completion)); } diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card.cpp index af37edb9c..ca2c864a9 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_card.cpp @@ -10,25 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "stripe/stripe_decode.h" namespace Stripe { +namespace { -Card::Card( - QString id, - QString last4, - CardBrand brand, - quint32 expMonth, - quint32 expYear) -: _cardId(id) -, _last4(last4) -, _brand(brand) -, _expMonth(expMonth) -, _expYear(expYear) { -} - -Card Card::Empty() { - return Card(QString(), QString(), CardBrand::Unknown, 0, 0); -} - -[[nodiscard]] CardBrand BrandFromString(const QString &brand) { +CardBrand BrandFromString(const QString &brand) { if (brand == "visa") { return CardBrand::Visa; } else if (brand == "american express") { @@ -46,7 +30,7 @@ Card Card::Empty() { } } -[[nodiscard]] CardFundingType FundingFromString(const QString &funding) { +CardFundingType FundingFromString(const QString &funding) { if (funding == "credit") { return CardFundingType::Credit; } else if (funding == "debit") { @@ -58,6 +42,25 @@ Card Card::Empty() { } } +} // namespace + +Card::Card( + QString id, + QString last4, + CardBrand brand, + quint32 expMonth, + quint32 expYear) +: _cardId(id) +, _last4(last4) +, _brand(brand) +, _expMonth(expMonth) +, _expYear(expYear) { +} + +Card Card::Empty() { + return Card(QString(), QString(), CardBrand::Unknown, 0, 0); +} + Card Card::DecodedObjectFromAPIResponse(QJsonObject object) { if (!ContainsFields(object, { u"id", @@ -80,7 +83,7 @@ Card Card::DecodedObjectFromAPIResponse(QJsonObject object) { auto result = Card(cardId, last4, brand, expMonth, expYear); result._name = string(u"name"); result._dynamicLast4 = string(u"dynamic_last4"); - result._funding = FundingFromString(string(u"funding")); + result._funding = FundingFromString(string(u"funding").toLower()); result._fingerprint = string(u"fingerprint"); result._country = string(u"country"); result._currency = string(u"currency"); @@ -97,6 +100,74 @@ Card Card::DecodedObjectFromAPIResponse(QJsonObject object) { return result; } +QString Card::cardId() const { + return _cardId; +} + +QString Card::name() const { + return _name; +} + +QString Card::last4() const { + return _last4; +} + +QString Card::dynamicLast4() const { + return _dynamicLast4; +} + +CardBrand Card::brand() const { + return _brand; +} + +CardFundingType Card::funding() const { + return _funding; +} + +QString Card::fingerprint() const { + return _fingerprint; +} + +QString Card::country() const { + return _country; +} + +QString Card::currency() const { + return _currency; +} + +quint32 Card::expMonth() const { + return _expMonth; +} + +quint32 Card::expYear() const { + return _expYear; +} + +QString Card::addressLine1() const { + return _addressLine1; +} + +QString Card::addressLine2() const { + return _addressLine2; +} + +QString Card::addressCity() const { + return _addressCity; +} + +QString Card::addressState() const { + return _addressState; +} + +QString Card::addressZip() const { + return _addressZip; +} + +QString Card::addressCountry() const { + return _addressCountry; +} + bool Card::empty() const { return _cardId.isEmpty(); } diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card.h b/Telegram/SourceFiles/payments/stripe/stripe_card.h index d7cb63627..0b9059262 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_card.h @@ -42,6 +42,24 @@ public: [[nodiscard]] static Card DecodedObjectFromAPIResponse( QJsonObject object); + [[nodiscard]] QString cardId() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString last4() const; + [[nodiscard]] QString dynamicLast4() const; + [[nodiscard]] CardBrand brand() const; + [[nodiscard]] CardFundingType funding() const; + [[nodiscard]] QString fingerprint() const; + [[nodiscard]] QString country() const; + [[nodiscard]] QString currency() const; + [[nodiscard]] quint32 expMonth() const; + [[nodiscard]] quint32 expYear() const; + [[nodiscard]] QString addressLine1() const; + [[nodiscard]] QString addressLine2() const; + [[nodiscard]] QString addressCity() const; + [[nodiscard]] QString addressState() const; + [[nodiscard]] QString addressZip() const; + [[nodiscard]] QString addressCountry() const; + [[nodiscard]] bool empty() const; [[nodiscard]] explicit operator bool() const { return !empty(); diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp index 06894fe87..81b72c4e0 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Stripe { -QString CardParams::RootObjectName() const { +QString CardParams::rootObjectName() { return "card"; } diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_params.h b/Telegram/SourceFiles/payments/stripe/stripe_card_params.h index 811ab67a8..d107dc57f 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card_params.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_params.h @@ -11,11 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Stripe { -class CardParams final : public FormEncodable { -public: - QString RootObjectName() const override; - std::map formFieldValues() const override; - +struct CardParams { QString number; quint32 expMonth = 0; quint32 expYear = 0; @@ -28,6 +24,9 @@ public: QString addressZip; QString addressCountry; QString currency; + + [[nodiscard]] static QString rootObjectName(); + [[nodiscard]] std::map formFieldValues() const; }; } // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h b/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h index 1a09d69b1..7cae5e250 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h @@ -14,11 +14,26 @@ namespace Stripe { class FormEncodable { public: - [[nodiscard]] virtual QString RootObjectName() const = 0; + [[nodiscard]] virtual QString rootObjectName() = 0; + [[nodiscard]] virtual std::map formFieldValues() = 0; +}; + +template +struct MakeEncodable final : FormEncodable { +public: + MakeEncodable(const T &value) : _value(value) { + } + + QString rootObjectName() override { + return _value.rootObjectName(); + } + std::map formFieldValues() override { + return _value.formFieldValues(); + } + +private: + const T &_value; - // TODO incomplete, not used: nested complex structures not supported. - [[nodiscard]] virtual std::map formFieldValues() const - = 0; }; } // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp index 8ce752108..a60425076 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp @@ -13,8 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Stripe { QByteArray FormEncoder::formEncodedDataForObject( - FormEncodable &object) { - const auto root = object.RootObjectName(); + FormEncodable &&object) { + const auto root = object.rootObjectName(); const auto values = object.formFieldValues(); auto result = QByteArray(); auto keys = std::vector(); diff --git a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h index 798de928c..79e28471d 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h @@ -14,7 +14,7 @@ namespace Stripe { class FormEncoder { public: [[nodiscard]] static QByteArray formEncodedDataForObject( - FormEncodable &object); + FormEncodable &&object); }; diff --git a/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h b/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h index f2b2867ae..a42e7921a 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_payment_configuration.h @@ -16,8 +16,11 @@ namespace Stripe { struct PaymentConfiguration { QString publishableKey; // PaymentMethodType additionalPaymentMethods; // Apply Pay - BillingAddressFields requiredBillingAddressFields - = BillingAddressFields::None; + + // TODO incomplete, not used. + //BillingAddressFields requiredBillingAddressFields + // = BillingAddressFields::None; + QString companyName; // QString appleMerchantIdentifier; // Apple Pay // bool smsAutofillDisabled = true; // Mobile only diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp new file mode 100644 index 000000000..e9c497336 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -0,0 +1,245 @@ +/* +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_edit_card.h" + +#include "payments/ui/payments_panel_delegate.h" +#include "passport/ui/passport_details_row.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.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 { +namespace { + +constexpr auto kMaxPostcodeSize = 10; + +[[nodiscard]] uint32 ExtractYear(const QString &value) { + return value.split('/').value(1).toInt() + 2000; +} + +[[nodiscard]] uint32 ExtractMonth(const QString &value) { + return value.split('/').value(0).toInt(); +} + +} // namespace + +EditCard::EditCard( + QWidget *parent, + const NativePaymentDetails &native, + CardField field, + not_null delegate) +: _delegate(delegate) +, _native(native) +, _scroll(this, st::passportPanelScroll) +, _topShadow(this) +, _bottomShadow(this) +, _done( + this, + tr::lng_about_done(), + st::passportPanelSaveValue) { + setupControls(); +} + +void EditCard::setFocus(CardField field) { + _focusField = field; + if (const auto control = controlForField(field)) { + _scroll->ensureWidgetVisible(control); + control->setFocusFast(); + } +} + +void EditCard::showError(CardField field) { + if (const auto control = controlForField(field)) { + _scroll->ensureWidgetVisible(control); + control->showError(QString()); + } +} + +void EditCard::setupControls() { + const auto inner = setupContent(); + + _done->addClickHandler([=] { + _delegate->panelValidateCard(collect()); + }); + + 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 EditCard::setupContent() { + const auto inner = _scroll->setOwnedWidget( + object_ptr(this)); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + inner->resizeToWidth(width); + }, inner->lifetime()); + + const auto showBox = [=](object_ptr box) { + _delegate->panelShowBox(std::move(box)); + }; + using Type = Passport::Ui::PanelDetailsType; + auto maxLabelWidth = 0; + accumulate_max( + maxLabelWidth, + Row::LabelWidth("Card Number")); + accumulate_max( + maxLabelWidth, + Row::LabelWidth("CVC")); + accumulate_max( + maxLabelWidth, + Row::LabelWidth("MM/YY")); + if (_native.needCardholderName) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth("Cardholder Name")); + } + if (_native.needCountry) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth("Billing Country")); + } + if (_native.needZip) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth("Billing Zip")); + } + _number = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + "Card Number", + maxLabelWidth, + QString(), + QString(), + 1024)); + _cvc = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + "CVC", + maxLabelWidth, + QString(), + QString(), + 1024)); + _expire = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + "MM/YY", + maxLabelWidth, + QString(), + QString(), + 1024)); + if (_native.needCardholderName) { + _name = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + "Cardholder Name", + maxLabelWidth, + QString(), + QString(), + 1024)); + } + if (_native.needCountry) { + _country = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Country, + "Billing Country", + maxLabelWidth, + QString(), + QString())); + } + if (_native.needZip) { + _zip = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Postcode, + "Billing Zip Code", + maxLabelWidth, + QString(), + QString(), + kMaxPostcodeSize)); + } + return inner; +} + +void EditCard::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void EditCard::focusInEvent(QFocusEvent *e) { + if (const auto control = controlForField(_focusField)) { + control->setFocusFast(); + } +} + +void EditCard::updateControlsGeometry() { + const auto submitTop = height() - _done->height(); + _scroll->setGeometry(0, 0, width(), submitTop); + _topShadow->resizeToWidth(width()); + _topShadow->moveToLeft(0, 0); + _bottomShadow->resizeToWidth(width()); + _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); + _done->setFullWidth(width()); + _done->moveToLeft(0, submitTop); + + _scroll->updateBars(); +} + +auto EditCard::controlForField(CardField field) const -> Row* { + switch (field) { + case CardField::Number: return _number; + case CardField::CVC: return _cvc; + case CardField::ExpireDate: return _expire; + case CardField::Name: return _name; + case CardField::AddressCountry: return _country; + case CardField::AddressZip: return _zip; + } + Unexpected("Unknown field in EditCard::controlForField."); +} + +UncheckedCardDetails EditCard::collect() const { + return { + .number = _number ? _number->valueCurrent() : QString(), + .cvc = _cvc ? _cvc->valueCurrent() : QString(), + .expireYear = _expire ? ExtractYear(_expire->valueCurrent()) : 0, + .expireMonth = _expire ? ExtractMonth(_expire->valueCurrent()) : 0, + .cardholderName = _name ? _name->valueCurrent() : QString(), + .addressCountry = _country ? _country->valueCurrent() : QString(), + .addressZip = _zip ? _zip->valueCurrent() : QString(), + }; +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h new file mode 100644 index 000000000..fc77be6d6 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -0,0 +1,73 @@ +/* +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 Passport::Ui { +class PanelDetailsRow; +} // namespace Passport::Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +class EditCard final : public RpWidget { +public: + EditCard( + QWidget *parent, + const NativePaymentDetails &native, + CardField field, + not_null delegate); + + void showError(CardField field); + void setFocus(CardField field); + +private: + using Row = Passport::Ui::PanelDetailsRow; + + void resizeEvent(QResizeEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + + void setupControls(); + [[nodiscard]] not_null setupContent(); + void updateControlsGeometry(); + [[nodiscard]] Row *controlForField(CardField field) const; + + [[nodiscard]] UncheckedCardDetails collect() const; + + const not_null _delegate; + NativePaymentDetails _native; + + object_ptr _scroll; + object_ptr _topShadow; + object_ptr _bottomShadow; + object_ptr _done; + + Row *_number = nullptr; + Row *_cvc = nullptr; + Row *_expire = nullptr; + Row *_name = nullptr; + Row *_country = nullptr; + Row *_zip = nullptr; + + CardField _focusField = CardField::Number; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 100a873d0..eff78b29b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -33,7 +33,7 @@ EditInformation::EditInformation( QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, - EditField field, + InformationField field, not_null delegate) : _delegate(delegate) , _invoice(invoice) @@ -48,7 +48,7 @@ EditInformation::EditInformation( setupControls(); } -void EditInformation::setFocus(EditField field) { +void EditInformation::setFocus(InformationField field) { _focusField = field; if (const auto control = controlForField(field)) { _scroll->ensureWidgetVisible(control); @@ -56,7 +56,7 @@ void EditInformation::setFocus(EditField field) { } } -void EditInformation::showError(EditField field) { +void EditInformation::showError(InformationField field) { if (const auto control = controlForField(field)) { _scroll->ensureWidgetVisible(control); control->showError(QString()); @@ -264,16 +264,16 @@ void EditInformation::updateControlsGeometry() { _scroll->updateBars(); } -auto EditInformation::controlForField(EditField field) const -> Row* { +auto EditInformation::controlForField(InformationField field) const -> Row* { switch (field) { - case EditField::ShippingStreet: return _street1; - case EditField::ShippingCity: return _city; - case EditField::ShippingState: return _state; - case EditField::ShippingCountry: return _country; - case EditField::ShippingPostcode: return _postcode; - case EditField::Name: return _name; - case EditField::Email: return _email; - case EditField::Phone: return _phone; + case InformationField::ShippingStreet: return _street1; + case InformationField::ShippingCity: return _city; + case InformationField::ShippingState: return _state; + case InformationField::ShippingCountry: return _country; + case InformationField::ShippingPostcode: return _postcode; + case InformationField::Name: return _name; + case InformationField::Email: return _email; + case InformationField::Phone: return _phone; } Unexpected("Unknown field in EditInformation::controlForField."); } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h index 8fb434074..d4f8ee78e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -33,11 +33,11 @@ public: QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, - EditField field, + InformationField field, not_null delegate); - void showError(EditField field); - void setFocus(EditField field); + void showError(InformationField field); + void setFocus(InformationField field); private: using Row = Passport::Ui::PanelDetailsRow; @@ -48,7 +48,7 @@ private: void setupControls(); [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); - [[nodiscard]] Row *controlForField(EditField field) const; + [[nodiscard]] Row *controlForField(InformationField field) const; [[nodiscard]] RequestedInformation collect() const; @@ -71,7 +71,7 @@ private: Row *_email = nullptr; Row *_phone = nullptr; - EditField _focusField = EditField::ShippingStreet; + InformationField _focusField = InformationField::ShippingStreet; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 41d44b4ee..0243f0acf 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -30,10 +30,12 @@ FormSummary::FormSummary( QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, + const NativePaymentDetails &native, const ShippingOptions &options, not_null delegate) : _delegate(delegate) , _invoice(invoice) +, _native(native) , _options(options) , _information(current) , _scroll(this, st::passportPanelScroll) @@ -134,6 +136,20 @@ not_null FormSummary::setupContent() { st::passportFormDividerHeight), { 0, 0, 0, st::passportFormHeaderPadding.top() }); + if (_native.supported) { + const auto method = inner->add(object_ptr(inner)); + method->addClickHandler([=] { + _delegate->panelEditPaymentMethod(); + }); + method->updateContent( + tr::lng_payments_payment_method(tr::now), + (_native.ready + ? _native.credentialsTitle + : tr::lng_payments_payment_method_ph(tr::now)), + _native.ready, + false, + anim::type::instant); + } if (_invoice.isShippingAddressRequested) { const auto info = inner->add(object_ptr(inner)); info->addClickHandler([=] { @@ -153,7 +169,9 @@ not_null FormSummary::setupContent() { push(_information.shippingAddress.postcode); info->updateContent( tr::lng_payments_shipping_address(tr::now), - (list.isEmpty() ? "enter pls" : list.join(", ")), + (list.isEmpty() + ? tr::lng_payments_shipping_address_ph(tr::now) + : list.join(", ")), !list.isEmpty(), false, anim::type::instant); @@ -167,7 +185,7 @@ not_null FormSummary::setupContent() { tr::lng_payments_shipping_method(tr::now), (selected != end(_options.list) ? selected->title - : "enter pls"), + : tr::lng_payments_shipping_method_ph(tr::now)), (selected != end(_options.list)), false, anim::type::instant); @@ -178,7 +196,7 @@ not_null FormSummary::setupContent() { name->updateContent( tr::lng_payments_info_name(tr::now), (_information.name.isEmpty() - ? "enter pls" + ? tr::lng_payments_info_name_ph(tr::now) : _information.name), !_information.name.isEmpty(), false, @@ -190,7 +208,7 @@ not_null FormSummary::setupContent() { email->updateContent( tr::lng_payments_info_email(tr::now), (_information.email.isEmpty() - ? "enter pls" + ? tr::lng_payments_info_email_ph(tr::now) : _information.email), !_information.email.isEmpty(), false, @@ -202,7 +220,7 @@ not_null FormSummary::setupContent() { phone->updateContent( tr::lng_payments_info_phone(tr::now), (_information.phone.isEmpty() - ? "enter pls" + ? tr::lng_payments_info_phone_ph(tr::now) : _information.phone), !_information.phone.isEmpty(), false, diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 32dd89bc7..60c7a5538 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -29,6 +29,7 @@ public: QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, + const NativePaymentDetails &native, const ShippingOptions &options, not_null delegate); @@ -44,6 +45,7 @@ private: const not_null _delegate; Invoice _invoice; + NativePaymentDetails _native; ShippingOptions _options; RequestedInformation _information; object_ptr _scroll; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 63d9ba40b..6f98b2b82 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_form_summary.h" #include "payments/ui/payments_edit_information.h" +#include "payments/ui/payments_edit_card.h" #include "payments/ui/payments_panel_delegate.h" #include "ui/widgets/separate_panel.h" #include "ui/boxes/single_choice_box.h" @@ -45,12 +46,14 @@ void Panel::requestActivate() { void Panel::showForm( const Invoice &invoice, const RequestedInformation ¤t, + const NativePaymentDetails &native, const ShippingOptions &options) { _widget->showInner( base::make_unique_q( _widget.get(), invoice, current, + native, options, _delegate)); _widget->setBackAllowed(false); @@ -59,29 +62,30 @@ void Panel::showForm( void Panel::showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, - EditField field) { + InformationField field) { auto edit = base::make_unique_q( _widget.get(), invoice, current, field, _delegate); - _weakEditWidget = edit.get(); + _weakEditInformation = edit.get(); _widget->showInner(std::move(edit)); _widget->setBackAllowed(true); - _weakEditWidget->setFocus(field); + _weakEditInformation->setFocus(field); } -void Panel::showEditError( +void Panel::showInformationError( const Invoice &invoice, const RequestedInformation ¤t, - EditField field) { - if (_weakEditWidget) { - _weakEditWidget->showError(field); + InformationField field) { + if (_weakEditInformation) { + _weakEditInformation->showError(field); } else { showEditInformation(invoice, current, field); - if (_weakEditWidget && field == EditField::ShippingCountry) { - _weakEditWidget->showError(field); + if (_weakEditInformation + && field == InformationField::ShippingCountry) { + _weakEditInformation->showError(field); } } } @@ -109,6 +113,57 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { })); } +void Panel::choosePaymentMethod(const NativePaymentDetails &native) { + Expects(native.supported); + + if (!native.ready) { + showEditCard(native, CardField::Number); + return; + } + const auto title = native.credentialsTitle; + showBox(Box([=](not_null box) { + const auto save = [=](int option) { + if (option) { + showEditCard(native, CardField::Number); + } + }; + SingleChoiceBox(box, { + .title = tr::lng_payments_payment_method(), + .options = { native.credentialsTitle, "New Card..." }, // #TODO payments lang + .initialSelection = 0, + .callback = save, + }); + })); +} + +void Panel::showEditCard( + const NativePaymentDetails &native, + CardField field) { + auto edit = base::make_unique_q( + _widget.get(), + native, + field, + _delegate); + _weakEditCard = edit.get(); + _widget->showInner(std::move(edit)); + _widget->setBackAllowed(true); + _weakEditCard->setFocus(field); +} + +void Panel::showCardError( + const NativePaymentDetails &native, + CardField field) { + if (_weakEditCard) { + _weakEditCard->showError(field); + } else { + showEditCard(native, field); + if (_weakEditCard + && field == CardField::AddressCountry) { + _weakEditCard->showError(field); + } + } +} + rpl::producer<> Panel::backRequests() const { return _widget->backRequests(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 9cd852731..6d7202591 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -22,8 +22,11 @@ class PanelDelegate; struct Invoice; struct RequestedInformation; struct ShippingOptions; -enum class EditField; +enum class InformationField; +enum class CardField; class EditInformation; +class EditCard; +struct NativePaymentDetails; class Panel final { public: @@ -35,16 +38,24 @@ public: void showForm( const Invoice &invoice, const RequestedInformation ¤t, + const NativePaymentDetails &native, const ShippingOptions &options); void showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, - EditField field); - void showEditError( + InformationField field); + void showInformationError( const Invoice &invoice, const RequestedInformation ¤t, - EditField field); + InformationField field); + void showEditCard( + const NativePaymentDetails &native, + CardField field); + void showCardError( + const NativePaymentDetails &native, + CardField field); void chooseShippingOption(const ShippingOptions &options); + void choosePaymentMethod(const NativePaymentDetails &native); [[nodiscard]] rpl::producer<> backRequests() const; @@ -56,7 +67,8 @@ public: private: const not_null _delegate; std::unique_ptr _widget; - QPointer _weakEditWidget; + QPointer _weakEditInformation; + QPointer _weakEditCard; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 0ab3c0d23..fba8e2dc1 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -104,19 +104,7 @@ struct RequestedInformation { } }; -struct SavedCredentials { - QString id; - QString title; - - [[nodiscard]] bool valid() const { - return !id.isEmpty(); - } - [[nodiscard]] explicit operator bool() const { - return valid(); - } -}; - -enum class EditField { +enum class InformationField { ShippingStreet, ShippingCity, ShippingState, @@ -127,4 +115,32 @@ enum class EditField { Phone, }; +struct NativePaymentDetails { + QString credentialsTitle; + bool ready = false; + bool supported = false; + bool needCountry = false; + bool needZip = false; + bool needCardholderName = false; +}; + +enum class CardField { + Number, + CVC, + ExpireDate, + Name, + AddressCountry, + AddressZip, +}; + +struct UncheckedCardDetails { + QString number; + QString cvc; + uint32 expireYear = 0; + uint32 expireMonth = 0; + QString cardholderName; + QString addressCountry; + QString addressZip; +}; + } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index f737dc2b4..9d50d481e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -21,6 +21,7 @@ namespace Payments::Ui { using namespace ::Ui; struct RequestedInformation; +struct UncheckedCardDetails; class PanelDelegate { public: @@ -30,6 +31,7 @@ public: virtual void panelWebviewMessage(const QJsonDocument &message) = 0; virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; + virtual void panelEditPaymentMethod() = 0; virtual void panelEditShippingInformation() = 0; virtual void panelEditName() = 0; virtual void panelEditEmail() = 0; @@ -38,6 +40,7 @@ public: virtual void panelChangeShippingOption(const QString &id) = 0; virtual void panelValidateInformation(RequestedInformation data) = 0; + virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0; virtual void panelShowBox(object_ptr box) = 0; }; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index de3044487..6545b0f0e 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -69,6 +69,8 @@ PRIVATE passport/ui/passport_form_row.cpp passport/ui/passport_form_row.h + payments/ui/payments_edit_card.cpp + payments/ui/payments_edit_card.h payments/ui/payments_edit_information.cpp payments/ui/payments_edit_information.h payments/ui/payments_form_summary.cpp From 56031a640295779b9f76881b987b39e4ee14df87 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 Mar 2021 20:58:52 +0400 Subject: [PATCH 018/127] Handle native / non-native payment methods (same way). --- Telegram/Resources/langs/lang.strings | 1 + .../payments/payments_checkout_process.cpp | 114 ++++---------- .../payments/payments_checkout_process.h | 10 +- .../SourceFiles/payments/payments_form.cpp | 148 +++++++++++++----- Telegram/SourceFiles/payments/payments_form.h | 92 ++++++----- .../payments/stripe/stripe_error.cpp | 24 +++ .../payments/stripe/stripe_error.h | 13 +- .../payments/ui/payments_edit_card.cpp | 2 +- .../payments/ui/payments_edit_card.h | 4 +- .../payments/ui/payments_form_summary.cpp | 30 ++-- .../payments/ui/payments_form_summary.h | 4 +- .../payments/ui/payments_panel.cpp | 91 +++++++++-- .../SourceFiles/payments/ui/payments_panel.h | 21 ++- .../payments/ui/payments_panel_data.h | 11 +- .../payments/ui/payments_panel_delegate.h | 2 + .../payments/ui/payments_webview.cpp | 97 ------------ .../payments/ui/payments_webview.h | 38 ----- .../SourceFiles/ui/widgets/separate_panel.cpp | 4 + .../SourceFiles/ui/widgets/separate_panel.h | 7 +- Telegram/cmake/td_ui.cmake | 2 - 20 files changed, 360 insertions(+), 355 deletions(-) delete mode 100644 Telegram/SourceFiles/payments/ui/payments_webview.cpp delete mode 100644 Telegram/SourceFiles/payments/ui/payments_webview.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 287c713a3..11bb8041c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1864,6 +1864,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_total_label" = "Total"; "lng_payments_pay_amount" = "Pay {amount}"; "lng_payments_payment_method" = "Payment Method"; +"lng_payments_new_card" = "New Card..."; "lng_payments_payment_method_ph" = "Enter your card details"; "lng_payments_shipping_address" = "Shipping Information"; "lng_payments_shipping_address_ph" = "Enter your shipping information"; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 4684896f6..d6ae27bb5 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 "main/main_domain.h" @@ -17,10 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history.h" #include "core/local_url_handlers.h" // TryConvertUrlToLocal. +#include "core/file_utilities.h" // File::OpenUrl. #include "apiwrap.h" -#include "stripe/stripe_api_client.h" -#include "stripe/stripe_error.h" -#include "stripe/stripe_token.h" // #TODO payments errors #include "mainwindow.h" @@ -57,13 +54,6 @@ base::flat_map, SessionProcesses> Processes; return result; } -[[nodiscard]] QString CardTitle(const Stripe::Card &card) { - // Like server stores saved_credentials title. - return Stripe::CardBrandToString(card.brand()).toLower() - + " *" - + card.last4(); -} - } // namespace void CheckoutProcess::Start(not_null item) { @@ -110,7 +100,7 @@ not_null CheckoutProcess::panelDelegate() { } void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { - v::match(update.data, [&](const FormReady &) { + v::match(update, [&](const FormReady &) { performInitialSilentValidation(); if (!_initialSilentValidation) { showForm(); @@ -124,17 +114,12 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _submitState = SubmitState::Validated; panelSubmit(); } + }, [&](const PaymentMethodUpdate&) { + showForm(); }, [&](const VerificationNeeded &info) { - if (_webviewWindow) { - _webviewWindow->navigate(info.url); - } else { - _webviewWindow = std::make_unique( - webviewDataPath(), - info.url, - panelDelegate()); - if (!_webviewWindow->shown()) { - // #TODO payments errors - } + if (!_panel->showWebview(info.url, false)) { + File::OpenUrl(info.url); + panelCloseSure(); } }, [&](const PaymentFinished &result) { const auto weak = base::make_weak(this); @@ -249,7 +234,7 @@ void CheckoutProcess::panelSubmit() { || _submitState == SubmitState::Finishing) { return; } - const auto &native = _form->nativePayment(); + const auto &method = _form->paymentMethod(); const auto &invoice = _form->invoice(); const auto &options = _form->shippingOptions(); if (!options.list.empty() && options.selectedId.isEmpty()) { @@ -264,24 +249,12 @@ void CheckoutProcess::panelSubmit() { _submitState = SubmitState::Validation; _form->validateInformation(_form->savedInformation()); return; - } else if (native - && !native.newCredentials - && !native.savedCredentials) { + } else if (!method.newCredentials && !method.savedCredentials) { editPaymentMethod(); return; } _submitState = SubmitState::Finishing; - if (!native) { - _webviewWindow = std::make_unique( - webviewDataPath(), - _form->details().url, - panelDelegate()); - if (!_webviewWindow->shown()) { - // #TODO payments errors - } - } else if (native.newCredentials) { - _form->send(native.newCredentials.data); - } + _form->submit(); } void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) { @@ -321,19 +294,25 @@ void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) { "Not an object received in payment credentials.")); return; } - const auto serializedCredentials = QJsonDocument( - credentials.toObject() - ).toJson(QJsonDocument::Compact); - - _form->send(serializedCredentials); + crl::on_main(this, [=] { + _form->setPaymentCredentials(NewCredentials{ + .title = title, + .data = QJsonDocument( + credentials.toObject() + ).toJson(QJsonDocument::Compact), + .saveOnServer = false, // #TODO payments save + }); + }); } bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { if (Core::TryConvertUrlToLocal(uri) == uri) { return true; } - panelCloseSure(); - App::wnd()->activate(); + crl::on_main(this, [=] { + panelCloseSure(); + App::wnd()->activate(); + }); return false; } @@ -346,46 +325,7 @@ void CheckoutProcess::panelEditPaymentMethod() { } void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) { - Expects(_form->nativePayment().type == NativePayment::Type::Stripe); - Expects(!_form->nativePayment().stripePublishableKey.isEmpty()); - - if (_stripe) { - return; - } - auto configuration = Stripe::PaymentConfiguration{ - .publishableKey = _form->nativePayment().stripePublishableKey, - .companyName = "Telegram", - }; - _stripe = std::make_unique(std::move(configuration)); - auto card = Stripe::CardParams{ - .number = data.number, - .expMonth = data.expireMonth, - .expYear = data.expireYear, - .cvc = data.cvc, - .name = data.cardholderName, - .addressZip = data.addressZip, - .addressCountry = data.addressCountry, - }; - _stripe->createTokenWithCard(std::move(card), crl::guard(this, [=]( - Stripe::Token token, - Stripe::Error error) { - _stripe = nullptr; - - if (error) { - int a = 0; - // #TODO payment errors - } else { - _form->setPaymentCredentials({ - .title = CardTitle(token.card()), - .data = QJsonDocument(QJsonObject{ - { "type", "card" }, - { "id", token.tokenId() }, - }).toJson(QJsonDocument::Compact), - .saveOnServer = false, - }); - showForm(); - } - })); + _form->validateCard(data); } void CheckoutProcess::panelEditShippingInformation() { @@ -408,7 +348,7 @@ void CheckoutProcess::showForm() { _panel->showForm( _form->invoice(), _form->savedInformation(), - _form->nativePayment().details, + _form->paymentMethod().ui, _form->shippingOptions()); } @@ -437,7 +377,7 @@ void CheckoutProcess::chooseShippingOption() { } void CheckoutProcess::editPaymentMethod() { - _panel->choosePaymentMethod(_form->nativePayment().details); + _panel->choosePaymentMethod(_form->paymentMethod().ui); } void CheckoutProcess::panelChooseShippingOption() { @@ -474,7 +414,7 @@ void CheckoutProcess::performInitialSilentValidation() { _form->validateInformation(saved); } -QString CheckoutProcess::webviewDataPath() const { +QString CheckoutProcess::panelWebviewDataPath() { return _session->domain().local().webviewDataPath(); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 3613ff232..636915071 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -12,17 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HistoryItem; -namespace Stripe { -class APIClient; -} // namespace Stripe - namespace Main { class Session; } // namespace Main namespace Payments::Ui { class Panel; -class WebviewWindow; enum class InformationField; } // namespace Payments::Ui @@ -67,7 +62,6 @@ private: void editPaymentMethod(); void performInitialSilentValidation(); - [[nodiscard]] QString webviewDataPath() const; void panelRequestClose() override; void panelCloseSure() override; @@ -87,11 +81,11 @@ private: void panelValidateCard(Ui::UncheckedCardDetails data) override; void panelShowBox(object_ptr box) override; + QString panelWebviewDataPath() override; + const not_null _session; const std::unique_ptr _form; const std::unique_ptr _panel; - std::unique_ptr _webviewWindow; - std::unique_ptr _stripe; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 62f9b59cb..b6ad2c95e 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -10,6 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "data/data_session.h" #include "apiwrap.h" +#include "stripe/stripe_api_client.h" +#include "stripe/stripe_error.h" +#include "stripe/stripe_token.h" #include #include @@ -67,6 +70,13 @@ namespace { MTP_string(information.shippingAddress.postcode))); } +[[nodiscard]] QString CardTitle(const Stripe::Card &card) { + // Like server stores saved_credentials title. + return Stripe::CardBrandToString(card.brand()).toLower() + + " *" + + card.last4(); +} + } // namespace Form::Form(not_null session, FullMsgId itemId) @@ -76,6 +86,8 @@ Form::Form(not_null session, FullMsgId itemId) requestForm(); } +Form::~Form() = default; + void Form::requestForm() { _api.request(MTPpayments_GetPaymentForm( MTP_int(_msgId) @@ -84,7 +96,7 @@ void Form::requestForm() { processForm(data); }); }).fail([=](const MTP::Error &error) { - _updates.fire({ Error{ Error::Type::Form, error.type() } }); + _updates.fire(Error{ Error::Type::Form, error.type() }); }).send(); } @@ -105,8 +117,8 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { processSavedCredentials(data); }); } - fillNativePaymentInformation(); - _updates.fire({ FormReady{} }); + fillPaymentMethodInformation(); + _updates.fire(FormReady{}); } void Form::processInvoice(const MTPDinvoice &data) { @@ -156,30 +168,32 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { void Form::processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data) { - // #TODO payments not yet supported + // #TODO payments save //_nativePayment.savedCredentials = SavedCredentials{ // .id = qs(data.vid()), // .title = qs(data.vtitle()), //}; - refreshNativePaymentDetails(); + refreshPaymentMethodDetails(); } -void Form::refreshNativePaymentDetails() { - const auto &saved = _nativePayment.savedCredentials; - const auto &entered = _nativePayment.newCredentials; - _nativePayment.details.credentialsTitle = entered - ? entered.title - : saved.title; - _nativePayment.details.ready = entered || saved; +void Form::refreshPaymentMethodDetails() { + const auto &saved = _paymentMethod.savedCredentials; + const auto &entered = _paymentMethod.newCredentials; + _paymentMethod.ui.title = entered ? entered.title : saved.title; + _paymentMethod.ui.ready = entered || saved; } -void Form::fillNativePaymentInformation() { - auto saved = std::move(_nativePayment.savedCredentials); - auto entered = std::move(_nativePayment.newCredentials); - _nativePayment = NativePayment(); - if (_details.nativeProvider != "stripe") { - return; +void Form::fillPaymentMethodInformation() { + _paymentMethod.native = NativePaymentMethod(); + _paymentMethod.ui.native = Ui::NativeMethodDetails(); + _paymentMethod.ui.url = _details.url; + if (_details.nativeProvider == "stripe") { + fillStripeNativeMethod(); } + refreshPaymentMethodDetails(); +} + +void Form::fillStripeNativeMethod() { auto error = QJsonParseError(); auto document = QJsonDocument::fromJson( _details.nativeParamsJson, @@ -202,22 +216,22 @@ void Form::fillNativePaymentInformation() { LOG(("Payment Error: No publishable_key in native_params.")); return; } - _nativePayment = NativePayment{ - .type = NativePayment::Type::Stripe, - .stripePublishableKey = key, - .savedCredentials = std::move(saved), - .newCredentials = std::move(entered), - .details = Ui::NativePaymentDetails{ - .supported = true, - .needCountry = value(u"need_country").toBool(), - .needZip = value(u"need_zip").toBool(), - .needCardholderName = value(u"need_cardholder_name").toBool(), + _paymentMethod.native = NativePaymentMethod{ + .data = StripePaymentMethod{ + .publishableKey = key, }, }; - refreshNativePaymentDetails(); + _paymentMethod.ui.native = Ui::NativeMethodDetails{ + .supported = true, + .needCountry = value(u"need_country").toBool(), + .needZip = value(u"need_zip").toBool(), + .needCardholderName = value(u"need_cardholder_name").toBool(), + }; } -void Form::send(const QByteArray &serializedCredentials) { +void Form::submit() { + Expects(!_paymentMethod.newCredentials.data.isEmpty()); // #TODO payments save + using Flag = MTPpayments_SendPaymentForm::Flag; _api.request(MTPpayments_SendPaymentForm( MTP_flags((_requestedInformationId.isEmpty() @@ -231,15 +245,15 @@ void Form::send(const QByteArray &serializedCredentials) { MTP_string(_shippingOptions.selectedId), MTP_inputPaymentCredentials( MTP_flags(0), - MTP_dataJSON(MTP_bytes(serializedCredentials))) + MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))) )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { - _updates.fire({ PaymentFinished{ data.vupdates() } }); + _updates.fire(PaymentFinished{ data.vupdates() }); }, [&](const MTPDpayments_paymentVerificationNeeded &data) { - _updates.fire({ VerificationNeeded{ qs(data.vurl()) } }); + _updates.fire(VerificationNeeded{ qs(data.vurl()) }); }); }).fail([=](const MTP::Error &error) { - _updates.fire({ Error{ Error::Type::Send, error.type() } }); + _updates.fire(Error{ Error::Type::Send, error.type() }); }).send(); } @@ -273,18 +287,76 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _shippingOptions.selectedId = _shippingOptions.list.front().id; } _savedInformation = _validatedInformation; - _updates.fire({ ValidateFinished{} }); + _updates.fire(ValidateFinished{}); }).fail([=](const MTP::Error &error) { _validateRequestId = 0; - _updates.fire({ Error{ Error::Type::Validate, error.type() } }); + _updates.fire(Error{ Error::Type::Validate, error.type() }); }).send(); } +void Form::validateCard(const Ui::UncheckedCardDetails &details) { + Expects(!v::is_null(_paymentMethod.native.data)); + + const auto &native = _paymentMethod.native.data; + if (const auto stripe = std::get_if(&native)) { + validateCard(*stripe, details); + } else { + Unexpected("Native payment provider in Form::validateCard."); + } +} + +void Form::validateCard( + const StripePaymentMethod &method, + const Ui::UncheckedCardDetails &details) { + Expects(!method.publishableKey.isEmpty()); + + if (_stripe) { + return; + } + auto configuration = Stripe::PaymentConfiguration{ + .publishableKey = method.publishableKey, + .companyName = "Telegram", + }; + _stripe = std::make_unique(std::move(configuration)); + auto card = Stripe::CardParams{ + .number = details.number, + .expMonth = details.expireMonth, + .expYear = details.expireYear, + .cvc = details.cvc, + .name = details.cardholderName, + .addressZip = details.addressZip, + .addressCountry = details.addressCountry, + }; + _stripe->createTokenWithCard(std::move(card), crl::guard(this, [=]( + Stripe::Token token, + Stripe::Error error) { + _stripe = nullptr; + + if (error) { + LOG(("Stripe Error %1: %2 (%3)" + ).arg(int(error.code()) + ).arg(error.description() + ).arg(error.message())); + _updates.fire(Error{ Error::Type::Stripe, error.description() }); + } else { + setPaymentCredentials({ + .title = CardTitle(token.card()), + .data = QJsonDocument(QJsonObject{ + { "type", "card" }, + { "id", token.tokenId() }, + }).toJson(QJsonDocument::Compact), + .saveOnServer = false, // #TODO payments save + }); + } + })); +} + void Form::setPaymentCredentials(const NewCredentials &credentials) { Expects(!credentials.empty()); - _nativePayment.newCredentials = credentials; - refreshNativePaymentDetails(); + _paymentMethod.newCredentials = credentials; + refreshPaymentMethodDetails(); + _updates.fire(PaymentMethodUpdate{}); } void Form::setShippingOption(const QString &id) { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index fbe4bd44f..7fbbe6c2a 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -8,8 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "payments/ui/payments_panel_data.h" +#include "base/weak_ptr.h" #include "mtproto/sender.h" +namespace Stripe { +class APIClient; +} // namespace Stripe + namespace Main { class Session; } // namespace Main @@ -58,55 +63,64 @@ struct NewCredentials { } }; -struct NativePayment { - enum class Type { - None, - Stripe, - }; - Type type = Type::None; - QString stripePublishableKey; - SavedCredentials savedCredentials; - NewCredentials newCredentials; - Ui::NativePaymentDetails details; +struct StripePaymentMethod { + QString publishableKey; +}; + +struct NativePaymentMethod { + std::variant< + v::null_t, + StripePaymentMethod> data; [[nodiscard]] bool valid() const { - return (type != Type::None); + return !v::is_null(data); } [[nodiscard]] explicit operator bool() const { return valid(); } }; +struct PaymentMethod { + NativePaymentMethod native; + SavedCredentials savedCredentials; + NewCredentials newCredentials; + Ui::PaymentMethodDetails ui; +}; + struct FormReady {}; struct ValidateFinished {}; -struct Error { - enum class Type { - Form, - Validate, - Send, - }; - Type type = Type::Form; - QString id; -}; +struct PaymentMethodUpdate {}; struct VerificationNeeded { QString url; }; struct PaymentFinished { MTPUpdates updates; }; - -struct FormUpdate { - std::variant< - FormReady, - VerificationNeeded, - ValidateFinished, - PaymentFinished, - Error> data; +struct Error { + enum class Type { + Form, + Validate, + Stripe, + Send, + }; + Type type = Type::Form; + QString id; }; -class Form final { +struct FormUpdate : std::variant< + FormReady, + ValidateFinished, + PaymentMethodUpdate, + VerificationNeeded, + PaymentFinished, + Error> { + using variant::variant; +}; + +class Form final : public base::has_weak_ptr { public: Form(not_null session, FullMsgId itemId); + ~Form(); [[nodiscard]] const Ui::Invoice &invoice() const { return _invoice; @@ -117,8 +131,8 @@ public: [[nodiscard]] const Ui::RequestedInformation &savedInformation() const { return _savedInformation; } - [[nodiscard]] const NativePayment &nativePayment() const { - return _nativePayment; + [[nodiscard]] const PaymentMethod &paymentMethod() const { + return _paymentMethod; } [[nodiscard]] const Ui::ShippingOptions &shippingOptions() const { return _shippingOptions; @@ -129,9 +143,10 @@ public: } void validateInformation(const Ui::RequestedInformation &information); + void validateCard(const Ui::UncheckedCardDetails &details); void setPaymentCredentials(const NewCredentials &credentials); void setShippingOption(const QString &id); - void send(const QByteArray &serializedCredentials); + void submit(); private: void requestForm(); @@ -142,8 +157,13 @@ private: void processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data); void processShippingOptions(const QVector &data); - void fillNativePaymentInformation(); - void refreshNativePaymentDetails(); + void fillPaymentMethodInformation(); + void fillStripeNativeMethod(); + void refreshPaymentMethodDetails(); + + void validateCard( + const StripePaymentMethod &method, + const Ui::UncheckedCardDetails &details); const not_null _session; MTP::Sender _api; @@ -152,11 +172,13 @@ private: Ui::Invoice _invoice; FormDetails _details; Ui::RequestedInformation _savedInformation; - NativePayment _nativePayment; + PaymentMethod _paymentMethod; Ui::RequestedInformation _validatedInformation; mtpRequestId _validateRequestId = 0; + std::unique_ptr _stripe; + Ui::ShippingOptions _shippingOptions; QString _requestedInformationId; diff --git a/Telegram/SourceFiles/payments/stripe/stripe_error.cpp b/Telegram/SourceFiles/payments/stripe/stripe_error.cpp index 675d41d8f..72f843252 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_error.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_error.cpp @@ -11,6 +11,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Stripe { +Error::Code Error::code() const { + return _code; +} + +QString Error::description() const { + return _description; +} + +QString Error::message() const { + return _message; +} + +QString Error::parameter() const { + return _parameter; +} + +Error Error::None() { + return Error(Code::None, {}, {}, {}); +} + Error Error::DecodedObjectFromResponse(QJsonObject object) { const auto entry = object.value("error"); if (!entry.isObject()) { @@ -80,4 +100,8 @@ Error Error::DecodedObjectFromResponse(QJsonObject object) { } } +bool Error::empty() const { + return (_code == Code::None); +} + } // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_error.h b/Telegram/SourceFiles/payments/stripe/stripe_error.h index 9a0a46ec3..36247d387 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_error.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_error.h @@ -42,14 +42,15 @@ public: , _parameter(parameter) { } - [[nodiscard]] static Error None() { - return Error(Code::None, QString(), QString()); - } + [[nodiscard]] Code code() const; + [[nodiscard]] QString description() const; + [[nodiscard]] QString message() const; + [[nodiscard]] QString parameter() const; + + [[nodiscard]] static Error None(); [[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object); - [[nodiscard]] bool empty() const { - return (_code == Code::None); - } + [[nodiscard]] bool empty() const; [[nodiscard]] explicit operator bool() const { return !empty(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index e9c497336..7d5918e9f 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -35,7 +35,7 @@ constexpr auto kMaxPostcodeSize = 10; EditCard::EditCard( QWidget *parent, - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field, not_null delegate) : _delegate(delegate) diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h index fc77be6d6..157e13edc 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -31,7 +31,7 @@ class EditCard final : public RpWidget { public: EditCard( QWidget *parent, - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field, not_null delegate); @@ -52,7 +52,7 @@ private: [[nodiscard]] UncheckedCardDetails collect() const; const not_null _delegate; - NativePaymentDetails _native; + NativeMethodDetails _native; object_ptr _scroll; object_ptr _topShadow; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 0243f0acf..dfcee629e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -30,12 +30,12 @@ FormSummary::FormSummary( QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, - const NativePaymentDetails &native, + const PaymentMethodDetails &method, const ShippingOptions &options, not_null delegate) : _delegate(delegate) , _invoice(invoice) -, _native(native) +, _method(method) , _options(options) , _information(current) , _scroll(this, st::passportPanelScroll) @@ -136,20 +136,18 @@ not_null FormSummary::setupContent() { st::passportFormDividerHeight), { 0, 0, 0, st::passportFormHeaderPadding.top() }); - if (_native.supported) { - const auto method = inner->add(object_ptr(inner)); - method->addClickHandler([=] { - _delegate->panelEditPaymentMethod(); - }); - method->updateContent( - tr::lng_payments_payment_method(tr::now), - (_native.ready - ? _native.credentialsTitle - : tr::lng_payments_payment_method_ph(tr::now)), - _native.ready, - false, - anim::type::instant); - } + const auto method = inner->add(object_ptr(inner)); + method->addClickHandler([=] { + _delegate->panelEditPaymentMethod(); + }); + method->updateContent( + tr::lng_payments_payment_method(tr::now), + (_method.ready + ? _method.title + : tr::lng_payments_payment_method_ph(tr::now)), + _method.ready, + false, + anim::type::instant); if (_invoice.isShippingAddressRequested) { const auto info = inner->add(object_ptr(inner)); info->addClickHandler([=] { diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 60c7a5538..38f30ff0f 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -29,7 +29,7 @@ public: QWidget *parent, const Invoice &invoice, const RequestedInformation ¤t, - const NativePaymentDetails &native, + const PaymentMethodDetails &method, const ShippingOptions &options, not_null delegate); @@ -45,7 +45,7 @@ private: const not_null _delegate; Invoice _invoice; - NativePaymentDetails _native; + PaymentMethodDetails _method; ShippingOptions _options; RequestedInformation _information; object_ptr _scroll; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 6f98b2b82..97c652e12 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/separate_panel.h" #include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" +#include "webview/webview_embed.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -37,7 +38,10 @@ Panel::Panel(not_null delegate) }, _widget->lifetime()); } -Panel::~Panel() = default; +Panel::~Panel() { + // Destroy _widget before _webview. + _widget = nullptr; +} void Panel::requestActivate() { _widget->showAndActivate(); @@ -46,14 +50,14 @@ void Panel::requestActivate() { void Panel::showForm( const Invoice &invoice, const RequestedInformation ¤t, - const NativePaymentDetails &native, + const PaymentMethodDetails &method, const ShippingOptions &options) { _widget->showInner( base::make_unique_q( _widget.get(), invoice, current, - native, + method, options, _delegate)); _widget->setBackAllowed(false); @@ -113,23 +117,84 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { })); } -void Panel::choosePaymentMethod(const NativePaymentDetails &native) { - Expects(native.supported); +void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { + if (method.native.supported) { + showEditCard(method.native, CardField::Number); + } else if (!showWebview(method.url, true)) { + // #TODO payments errors not supported + } +} - if (!native.ready) { - showEditCard(native, CardField::Number); +bool Panel::showWebview(const QString &url, bool allowBack) { + if (!_webview && !createWebview()) { + return false; + } + _webview->navigate(url); + _widget->setBackAllowed(allowBack); + return true; +} + +bool Panel::createWebview() { + auto container = base::make_unique_q(_widget.get()); + + container->setGeometry(_widget->innerGeometry()); + container->show(); + + _webview = std::make_unique( + container.get(), + Webview::WindowConfig{ + .userDataPath = _delegate->panelWebviewDataPath(), + }); + const auto raw = _webview.get(); + QObject::connect(container.get(), &QObject::destroyed, [=] { + if (_webview.get() == raw) { + _webview = nullptr; + } + }); + if (!raw->widget()) { + return false; + } + + container->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + raw->widget()->setGeometry(geometry); + }, container->lifetime()); + + raw->setMessageHandler([=](const QJsonDocument &message) { + _delegate->panelWebviewMessage(message); + }); + + raw->setNavigationHandler([=](const QString &uri) { + return _delegate->panelWebviewNavigationAttempt(uri); + }); + + raw->init(R"( +window.TelegramWebviewProxy = { +postEvent: function(eventType, eventData) { + if (window.external && window.external.invoke) { + window.external.invoke(JSON.stringify([eventType, eventData])); + } +} +};)"); + + _widget->showInner(std::move(container)); + return true; +} + +void Panel::choosePaymentMethod(const PaymentMethodDetails &method) { + if (!method.ready) { + showEditPaymentMethod(method); return; } - const auto title = native.credentialsTitle; showBox(Box([=](not_null box) { const auto save = [=](int option) { if (option) { - showEditCard(native, CardField::Number); + showEditPaymentMethod(method); } }; SingleChoiceBox(box, { .title = tr::lng_payments_payment_method(), - .options = { native.credentialsTitle, "New Card..." }, // #TODO payments lang + .options = { method.title, tr::lng_payments_new_card(tr::now) }, .initialSelection = 0, .callback = save, }); @@ -137,8 +202,10 @@ void Panel::choosePaymentMethod(const NativePaymentDetails &native) { } void Panel::showEditCard( - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field) { + Expects(native.supported); + auto edit = base::make_unique_q( _widget.get(), native, @@ -151,7 +218,7 @@ void Panel::showEditCard( } void Panel::showCardError( - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field) { if (_weakEditCard) { _weakEditCard->showError(field); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 6d7202591..a8a694a89 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -14,6 +14,10 @@ class SeparatePanel; class BoxContent; } // namespace Ui +namespace Webview { +class Window; +} // namespace Webview + namespace Payments::Ui { using namespace ::Ui; @@ -26,7 +30,8 @@ enum class InformationField; enum class CardField; class EditInformation; class EditCard; -struct NativePaymentDetails; +struct PaymentMethodDetails; +struct NativeMethodDetails; class Panel final { public: @@ -38,7 +43,7 @@ public: void showForm( const Invoice &invoice, const RequestedInformation ¤t, - const NativePaymentDetails &native, + const PaymentMethodDetails &method, const ShippingOptions &options); void showEditInformation( const Invoice &invoice, @@ -48,14 +53,17 @@ public: const Invoice &invoice, const RequestedInformation ¤t, InformationField field); + void showEditPaymentMethod(const PaymentMethodDetails &method); void showEditCard( - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field); void showCardError( - const NativePaymentDetails &native, + const NativeMethodDetails &native, CardField field); void chooseShippingOption(const ShippingOptions &options); - void choosePaymentMethod(const NativePaymentDetails &native); + void choosePaymentMethod(const PaymentMethodDetails &method); + + bool showWebview(const QString &url, bool allowBack); [[nodiscard]] rpl::producer<> backRequests() const; @@ -65,8 +73,11 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: + bool createWebview(); + const not_null _delegate; std::unique_ptr _widget; + std::unique_ptr _webview; QPointer _weakEditInformation; QPointer _weakEditCard; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index fba8e2dc1..7c3964c29 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -115,15 +115,20 @@ enum class InformationField { Phone, }; -struct NativePaymentDetails { - QString credentialsTitle; - bool ready = false; +struct NativeMethodDetails { bool supported = false; bool needCountry = false; bool needZip = false; bool needCardholderName = false; }; +struct PaymentMethodDetails { + QString title; + NativeMethodDetails native; + QString url; + bool ready = false; +}; + enum class CardField { Number, CVC, diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index 9d50d481e..8b6124604 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -42,6 +42,8 @@ public: virtual void panelValidateInformation(RequestedInformation data) = 0; virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0; virtual void panelShowBox(object_ptr box) = 0; + + virtual QString panelWebviewDataPath() = 0; }; } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_webview.cpp b/Telegram/SourceFiles/payments/ui/payments_webview.cpp deleted file mode 100644 index 39ae11b6c..000000000 --- a/Telegram/SourceFiles/payments/ui/payments_webview.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* -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 &userDataPath, - 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()); - - const auto path = - _webview = Ui::CreateChild( - window, - window, - Webview::WindowConfig{ .userDataPath = userDataPath }); - 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 deleted file mode 100644 index 823d11775..000000000 --- a/Telegram/SourceFiles/payments/ui/payments_webview.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -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 &userDataPath, - 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 b05c98f93..798c825d5 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp @@ -344,6 +344,10 @@ void SeparatePanel::setInnerSize(QSize size) { } } +QRect SeparatePanel::innerGeometry() const { + return _body->geometry(); +} + void SeparatePanel::initGeometry(QSize size) { const auto active = QApplication::activeWindow(); const auto center = !active diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.h b/Telegram/SourceFiles/ui/widgets/separate_panel.h index 721674d29..8c185f98c 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.h +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.h @@ -28,6 +28,7 @@ public: void setTitle(rpl::producer title); void setInnerSize(QSize size); + [[nodiscard]] QRect innerGeometry() const; void setHideOnDeactivate(bool hideOnDeactivate); void showAndActivate(); @@ -41,9 +42,9 @@ public: void showToast(const TextWithEntities &text); void destroyLayer(); - rpl::producer<> backRequests() const; - rpl::producer<> closeRequests() const; - rpl::producer<> closeEvents() const; + [[nodiscard]] rpl::producer<> backRequests() const; + [[nodiscard]] rpl::producer<> closeRequests() const; + [[nodiscard]] rpl::producer<> closeEvents() const; void setBackAllowed(bool allowed); protected: diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 6545b0f0e..fd8ada4b0 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -79,8 +79,6 @@ PRIVATE 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 From fafea73ea7ec15f91e5a0e7713d81b34dc6e4ea6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Mar 2021 17:05:31 +0400 Subject: [PATCH 019/127] Improve checkout main page design. --- .../icons/payments/payment_address.png | Bin 0 -> 802 bytes .../icons/payments/payment_address@2x.png | Bin 0 -> 1638 bytes .../icons/payments/payment_address@3x.png | Bin 0 -> 2402 bytes .../Resources/icons/payments/payment_card.png | Bin 0 -> 357 bytes .../icons/payments/payment_card@2x.png | Bin 0 -> 560 bytes .../icons/payments/payment_card@3x.png | Bin 0 -> 985 bytes .../icons/payments/payment_email.png | Bin 0 -> 949 bytes .../icons/payments/payment_email@2x.png | Bin 0 -> 1936 bytes .../icons/payments/payment_email@3x.png | Bin 0 -> 2860 bytes .../Resources/icons/payments/payment_name.png | Bin 0 -> 473 bytes .../icons/payments/payment_name@2x.png | Bin 0 -> 884 bytes .../icons/payments/payment_name@3x.png | Bin 0 -> 1411 bytes .../icons/payments/payment_phone.png | Bin 0 -> 711 bytes .../icons/payments/payment_phone@2x.png | Bin 0 -> 1327 bytes .../icons/payments/payment_phone@3x.png | Bin 0 -> 2090 bytes .../icons/payments/payment_shipping.png | Bin 0 -> 526 bytes .../icons/payments/payment_shipping@2x.png | Bin 0 -> 1022 bytes .../icons/payments/payment_shipping@3x.png | Bin 0 -> 1469 bytes Telegram/Resources/langs/lang.strings | 27 +- .../payments/payments_checkout_process.cpp | 15 +- .../SourceFiles/payments/payments_form.cpp | 120 ++++++- Telegram/SourceFiles/payments/payments_form.h | 31 +- .../SourceFiles/payments/ui/payments.style | 46 ++- .../payments/ui/payments_form_summary.cpp | 303 ++++++++++++------ .../payments/ui/payments_form_summary.h | 11 +- .../payments/ui/payments_panel.cpp | 23 +- .../SourceFiles/payments/ui/payments_panel.h | 3 + .../payments/ui/payments_panel_data.h | 9 + 28 files changed, 448 insertions(+), 140 deletions(-) create mode 100644 Telegram/Resources/icons/payments/payment_address.png create mode 100644 Telegram/Resources/icons/payments/payment_address@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_address@3x.png create mode 100644 Telegram/Resources/icons/payments/payment_card.png create mode 100644 Telegram/Resources/icons/payments/payment_card@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_card@3x.png create mode 100644 Telegram/Resources/icons/payments/payment_email.png create mode 100644 Telegram/Resources/icons/payments/payment_email@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_email@3x.png create mode 100644 Telegram/Resources/icons/payments/payment_name.png create mode 100644 Telegram/Resources/icons/payments/payment_name@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_name@3x.png create mode 100644 Telegram/Resources/icons/payments/payment_phone.png create mode 100644 Telegram/Resources/icons/payments/payment_phone@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_phone@3x.png create mode 100644 Telegram/Resources/icons/payments/payment_shipping.png create mode 100644 Telegram/Resources/icons/payments/payment_shipping@2x.png create mode 100644 Telegram/Resources/icons/payments/payment_shipping@3x.png diff --git a/Telegram/Resources/icons/payments/payment_address.png b/Telegram/Resources/icons/payments/payment_address.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc9b2f812b1e04c01e2d48d31cc77be8a7ee45f GIT binary patch literal 802 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZv>fz|`aE;usRa z`8L$QyRcAXpR$fxq=v89<^;z@YN{tq*nMPVeyF&wlx4et#{#&v|ZhzPP^b`z|4t#v`TM4CeS&YS$PR++>Wk zPB5F@>Y}6=q9wZU`fJYaqfO#PS1vHS+mdSr6|dT{+r(P@%TA zcH_6_&()U(X>zq6PT733LeZVqmSw|?iy2Ng8!l!n*}mOeO;3a?Wn+Y&(Bg0%G0ASp zGg$@;a}?zIjUOf$D6GD^%8P?%1IL+vw;D>^{r&x;PDn88Fm2v%_Tk^Zb1D4%{M9M_ z{ry+of3H-W&du0;V9lO7rSN~8X`3Y{Tgvp=8g+O##`y68b$t4@G3Kjng2A8GGtY|) zxt{ymkoc+A@+;g_`Qj9b^`epT>d@9%(Q=@tIRCW8#j~faqc!l=g*WYnB zZ{g*aZ*KLvIq%rJ_ipTI;r`=?r$3UMFuniTD(ee5W|am~y>hA+VLeBaVy30_Cq6Mf z+FVpz?7aGFmv~WOK;c7%b=R|_^rok5zb(2kS})b9;nY*3`LA-!R(s7j{Zt}aYih{G zS-dPvjQdufO>>^;@kHh0-@mrC_4S|g?%usS_lWG9MZfqG6egeiFn901eV4v|{mKsv zc;_XTU&{Yw4y=ry%{cj_%iFTqtgpUWb^iZtE%an-RB!d#u3O#tcp$1D8B3 zIN_wPudmZmzb9+!Dbp_v7Zv%|lrbOpRbwaB>!!nz__Cqzc(Txa@v5U=r5iY9Hk?d3 zr1rsZt{*3R==-xdAAglO2npUV`|{_H&(E59d*a^j@47toiPIlD?-l#H{%>F1-MIPv Z0r}8Co*%Q~loCIE^gT7m!o literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_address@2x.png b/Telegram/Resources/icons/payments/payment_address@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4cea3d3917292becafc8f15bba2ccac1e31e15 GIT binary patch literal 1638 zcmV-s2ATPZP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuB&`Cr=RCodHS~*B0O%yKgW4&)g z@xBmI@CZC$H!#v%5DgR~F*i_LQ^C|k)WARy%tS;DRK)w(_1HyJF!4fEP!Yj{`s*9E zwW@oj9jzLjALl{SRbBPI_r2Gzs=KP&jSxcF@nyyR%XA6oex6KCO_Am0WpaOi{}J>oiy>6rG-)HaH@C0hqv69R#<3fb1kM7}FTLNl8ft zu;b%nGCDd+W@l%~@87>IWB&5;($X(1EX*<|LJ34-XGIIXP*#(Tp^$tE;1IBDC8M`a%b~dVvktVgXQfr~&%<^QWFG9UB{?p`oGg zCJJ3(=moZ%DgYp=pXnziCaAl+yEIW?0E-vck_oIXc=8$0*4D=Jqen+al+6dy&TB-0 z1x#LGQ)`zz%LM>;=@7<;zkZR@($Z%h1qB6` zb^r4PD+M@OU0rRZ#rzU`#0nkO*Vl#mdn(cypeKP%Pe(~fNr?cxwzekJ-&-*qvkcU< zJvB8|7?*4(m$DrmQ=Qi^t+iu2?D6qYu=UzIhcz54*UTNXqF``tV`D=g*3i&k=wlC# zS)aznM#09)%8Du*@&RaVZFLkmi<4OPZf~C9U2-ENY2d6s3J%g78V5T{{DVt*o?cH0OaTA3nWpgs;ZJET3cId zz|G0Yk;dixRucdKj*N^5WU;sj3JQ8R*^rPB%Dxv0n8U-v@5bYt<}uU-ASETm@Hwoz zyPG%mrgC3jpWr-O4AF#ygg0T>aGJ+Z7XTP*Z*Lb!qr%?$c!PG8Yieo?I4vzLcIn#T zG>@Y;0KUGy^v|C^0(tD?XJlmXMor7;tN+fhwY5cke0)sx-srf7r8WQ{k)54Qk*>U8 zlfN)PwkHMMo@o}rE?B(S}+1F!3|K8qSGx93h92Wrm)L_jieBtFk zo?zav)(;;>9r$B}7CgrV09gP>^;_q-&1{dKVv`d?g8dU@xbv&4tER(6;gE1_06-Lz zpMEg|k9T%<==S!u!TG;`|I+B_XzdO;`t0}sAPCqaV1JJooYh}oJ4|*xE&f*-ppj+U z*c6K-TY@J5UVpXzA9Vhh4A7kM^Yf!!U0oDQT1}pwo-9ucvqz++ullU?Cn-kGZo$uz zY*k0t{{@7-wg`ekU;Q$^a+0 k37<;Al>wg`ekU;gAB0~NgM~K+1IU9mF91pL12_*Lpqu~&062&cfCJQ! zgEk`czrF56j{n&Qz&ceoEI7r0g^8g}BxLQ1dF}Cw$D6kC@t#LSE*TD_JK88o#D6vl zsyf3xAUSVzCAE?WGsP+2)*_rUH8j*@y)0|56fr=gYh`rCmM?87EsU10unDeanuXVzB50!H=JQYr4&r{-=={K?%7WTCiF1O!EyBWo@O_7#furYLvcfVcvE zaCGvvA_X>E1nK)q=8A+MRcA$JNC1j(l^aRj6`th^8X#PaWI~ZJ+?+_y8T{I0M=#MZ zA->A%O+lq$0(YecHM!Ud9l7_jBXV;Iv$VM~QZv8UfA7V&Zwq~uBF!*&3l&>)()H=Z zdfSG@-VDU27Pp?iu24I_LfpOYmuf?$)XtzqlTI;~l&tXr{QnYb@x-X3FdbhoyTBQT?eP@y~qu5O7I3eC@UZ%BO|xjq;NC zp-G4C&96c^tg( zs%_!lVf4b4&w9EbyNOli+2`oJDg5G#Q=Oq+TP(&!Gj99~IXpszwq(Gm2bt^$kYi9% z7ia&Ter7$b(U=1;DMWaigqgSC4(CKqoW5;D)c4Pq92@KuSMzzeThAD0?mq-hX@}iq zNz^oLC-Cq;ED3RMK(`X=RB5RWqNcNckY^F=V+>6Zj31dMv+z2hOO~;o ziFuD7t0-T61rPrHE2^S-c$O0JPy&@zt{ZI^ePTS2UjN!=JM=|7*Q=+PQjL5tlyR3v z!xI}gvo^o5)57J!VdhOFpmC1%W$A5c*Iz1!?YGG>yEO37`&H=1q9X+cy+*xQZUHH3 zF;IlnXai}9kk?o`b~a#7*oJi7MaZ7~$gk&!5KJgKHgLYi0Wtfg5H+JsBo{Nm)wPvgkCn3~eaHw}IpK!Zb#0E=|rO6)9anwwUg)9kv4qyj4M4 z9(s@grfXWUs!uu$agJ9#ABo(_Qd_LZtUDlUbh?xJNOht1dw7%KXMFzxw`*o>OmSc2l=-Q|6*@yEzAiT+`)g^UT3P(eBvD2 zJh^<5I2v!Rj(MfrWjD*tK6K)l%}g-|rl8?pCUWY0e<0LdBvwc3sCIr^B@LYKwViun zb)Av<%Aw5#dXCqoDb9k3hV&s z`cA1di|*EI!&W<8!3*4Jno%2zxgNc#j61+n@diFrZl0p^lf2BlCTCJE`pE+wLC#<) z01C!1 z=4okdF@<+ENl>C>^!DE}rWC67UoA_fSZuClgDZ~o1k|-k@$OCBCIv=qedyG>{dAzqGWweE&g!-BX2t@J}lVf zw~S+=kEJ#jFGrsd<_a||c?G*7Q20%C8cIk7A63x+O6SW0T;q2b;gI$E1}o2zjE@@L zRas{5^FvNUyK)>THSmOk3=ht=Kh(IN4G2K?_Q30dXP@uRek8}r=l!bjX?Z_w6vmI?m{d_$ai>OonE!j#*=|Hi@;Ytn4w9{&$Mnr6rV literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_card.png b/Telegram/Resources/icons/payments/payment_card.png new file mode 100644 index 0000000000000000000000000000000000000000..6e80687c6c0fa72a7ff623dada48d43a664df476 GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZp6?&U?BzhD30_ z4Rz!?qQKL0`qASr$@@a)KTKOqYXCITDL2|Hhdgdoe=v;_LH@>UcG_!_Fez~^Syb+cy{4qz6i!=4X+$DmzwIu zFOOpOzs{;1BekH~UtZqL#!0(t!on-dPMu~4S}6X%*8FPXlABwrW8XI3b&{Gdn!q=M Y*}lQ8O2(p{r3Bc1>P_aArP+3{I^NBQH63XJM^xVCx4tW3JFMEG{$sg5$ur8&lLesG@t z;~m3vg7Khe05^-d!zAV>4G*|hur^6M%w7=Xpeqp5_(<%I8|mj}Y#SJaPd}DEKhb>aY^D9v8I%~J8#(4Mr0uj#FAh(r zj$wGcTlizWbMGTVcJJAefEmg8mHa)5=1|vu>GwQZf~2+_ATjC$^U0(O}~rYj@7v``^ty) zM-R*QwAf2+-uFw!rOj5Rm0?B!(=Ep8LtnSNsEGP}CH;v@UIN23S@G(n{%tyqs~zpx sVmF!_2_1-bjkGIbSUqEVg(Y5g{k<|rHrYvHxuDqcboFyt=akR{0L#|ZWB>pF literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_card@3x.png b/Telegram/Resources/icons/payments/payment_card@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ae50fe67da0317d0512f76066707c3c6e19cf0b6 GIT binary patch literal 985 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;}uz(rC1}R)=9PbBI?(FH}7*fIb zcFx&eB|`zXvswKr40AY{^;iz}nka}FDR8K`h^QQx${q9I((90FJ zpllPdb@RU)eCaV*JSAwlexCx9ebXnSx3d{oS?onM8km|mS(ZQNThNfebnxX)W(|cI z4h2>37`Qmp1m?`MCQ&*{Y(v!AOLhDGr=M26`~Lft(z^)vOd zE6c6s8of7b`=VsK&~oqN#E1G~-S5hFZ*~0FueU_~<=Q5eISiH4ws8D^>{zvWgKz;; zRMpL&4*!1zSFyZj>S_3s_I2yEwEF3BfkFw}7ry>#b$!Y4*I&JM-_`S7e%VB-m#xrq zZP4Z`aZ`FBX{nbKBY`6yU=`Vg&QKQ0S*R@55C-C6cO-Iu&DjIdqLQL@s>}=rYneeCd+UcG&$=oW}rHA zM#J&PfjWxyM|NA-x18K}>vz@GFD$|Zd*ix)n$PwvtmBwmyY>6mthby+Hx%Tv^ya3% zn0G#1UEQ~>B)~aq>a3;TE=`Ct&biXcdBnLYt?yauE6D@NtJJ0~bLHBX<0*H=Y@>;; z7MB3$pH)rypZ|Poiu*C&TO?p_&BebIf39+gTe~DWM4fh;fO@ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_email.png b/Telegram/Resources/icons/payments/payment_email.png new file mode 100644 index 0000000000000000000000000000000000000000..41154157ba10006e187bdc00aa37f44b24c2290f GIT binary patch literal 949 zcmV;m14{gfP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?A4x<(R9Fe^RXa;-K@i>~qNeac zARrOa_zzSp#70eL#1I72Xlg;RiH|}tg^HL;e}SE8Y{XI}MVgQzLJA8l#4;jiLL%`s zcfY}1&P~oqj<>ir8Q8PSH?uR}&g{-{2q8$0UlZrAMg9)lBGAgfk228K)+XlW=EU*w zvADat6HiZ1qFgSEo0}VPa&jVOW@ZFzekyo|O-DE!rrXh7i`ud{x_jei{9c{MHJOlDF>6+u@ z2F5fsHANQ}7xeP-LeJ07bb5MPSNB9B!Q?ZU%wKzre!kb{1R{|LJ3!5iY2Vq|VZ8Q( zEUm1p=wKd?hY7-Ne0*Hvwh73m#xZc=)TUrC$XK;nCDgaCuaC#t+8O}^iNV1^ZNIm- z$M%PZhZ)zuYaW|wPyeVuhEhjOk6UteEqOz8}>IRV*Z*kM;! zm)=||l^Dax4Gwti^Z8hZk}2K5zyM=-o(ij-EjwXnXQ#9kqP@MHXMChLy{=t291e#3 ze!o^pGQ5`eL1VQGO9BrM4{U36C~q`l3kwUie#HFzJYyiD$ai#fFeaa4oxKq-O;0kJ zS#vsvv!2CiM#0c?QA11S^= zBu}I_?e6X}ZuUu~QgtvIQYaMC`)w0IBT_HHm<2ePo}Q-5%S&!TXhwJ)Xim^YqfyG| z^Hix+D3{C8?Cfl#;Lw?80C*q}&?j12T59I#U-i}mKmzrJAeCsZ=x~il|5WFa8{%?U zN;;huudlCSVq!v!jg5)L#YNHG-7VywPkelQh+?tGdvW{KQ?ztup|Eu#{MGPdX(s3A z=eGPwF3L_X6wq*%nV_SiBSL#cheQ8IdDJ*NJ8QV#5SZ=IzOeW;vFwEZi&2X}D+7N3 XYIMVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuC`bk7VRCodHT1!Y)K@=WMNzyEZ z9!gX~Z3<1Imo7@n9t2TQVvAad3!$=z7Dc(pYN3%45n{FyY7r5&h#-iJ9zGy@SR|F0 zT9}nuphj7z`7T|VnLGFT-}_(vfABx>bLVm9oHO6N&Ybz97-PhL6PBVUY|sC9JOV&u z225}SB1>R`PXHE~0g)Ln!4ZfofeAhVSj>0ipz0HUIz=-9Di^!M*yZC`)>{7E-%+@O+@5?Zxt70sD5hbB*+ObH1I zl#-G{Yu2nW>h9dRLwsM*%J}o=4;2>|`wc?y08E)Og|1z@rZxKW=g)NT;6YbwdFITS zbm-6_`ug>&RwkB&vW|nv2L}K~-mdc(E?l7a_;}C8VFNsO?i^v{RXTa{q-VJbc)S(uV23=j3742w{PE4ZEdYlH)+x&ZCkU0yLRnT)zZ$LJ007Y zZEe2p0f2|6Iyuw@oK^Q_wQ-Lh7FFgZr!>iHGBU2c`&oUJpg58 zWs(8i-QBjkJ*MZWQ>P^U(K6Uqjvqg63j)7aSeC-JZQE@5od$;2xd%Y??GGF{V5_{m zynM9&+SVUFeArg*rAwEjcDHZew&fkAkL~~f`Xa%@!;;V`*}8SB$ZDi9x)(2Aq-D#N z5gaJ|mM>pURaI4%69pdOiWMugvNJO?CE3^<xkNbg08#ivWLB3Qk5#ptgNf6Gn2-5;lhPdKG2LS)V_TABB73(99Lxd zy?XUZ;=`KP@#)I4va+PMPoF;3wuRFrQd3itmQSZcD6<+bR9Xc7`t?iHNii`o79O-8 z8XA)Lu-Q4u73+AYOlk`kSgbeGsi~RYV>fxTERL(CmQr;<}@^WWN%O2=pXdC}8bPWKa zw<0ZAut3a?q#r(fIFd$!AD>d*DWme*0pKLVO`WTC1BXQTN2l_HA!@#9C4)o^lf&Z{!b zJUWkig^nsKD=qDC0Tb!|{reUkQ?|p`9RN$0E){hWP6fV6X%*vErv^-rudc2(0;7m2 zV8_qS&Ni;dl3*o+b`=#B1Xmoh0Zxxf`}gnH+TqZ7@Vf&5{CKnw$w@IrZak>Bx7Ua; zd+3jQ0O0MVjq!)gBlaka)xlJX1uOOnwhv_W(f2(W6I2JB-jZ zc77)VFdM*wF#5}vFO8TYfJ8*+>eZ`+paytU88c?gpwp*M)8ogFjSw2VOteRt-TtG> zaL9WD0K2?;9sU0OJ1ttY$kG7^<>A2V4FGfsQCjuU0zpo@AV!!1;T-^U2#%0SnAQaa z1>rEdf!Gin0004RbvlBRGTbD zn>P7sBS%h@CuPJ6&uQ}sOg$z&A;25q!BsKZ&%FNtn8xB&2p%9FEmY6Z4H)nsO__dw zecc1Nakj)CEm%%Y4&$+Vh9C<5CWjyYHwd!}t3g<2XD2HvDq?r<-nEq-OvShX5R8#y z+17PPY%Gllh3p6bp+G&HbVg=CIE_XqY>CW(P@o=8IwLb6oJJ!QwnSz?C{Pb4o&Nw{ Wl>Vhpp0H{F00006PZC|v@m6PO|oPi`@W2kE!m!}h)@`cN4Cg1 zmLwueugMZ7`|?K0H+_G+@9*!AbI&>Vo_l`hoO{ngw!otKdBk`C0Q{FQ##Ss$_zhef ztd*YSugO9XZ!5GRs2mhuWHH9h_)9M4=HN7o<^qs7A~^J0gau+O0D!#^0@zs?@_Vn4 z?cY;EA^ZR6--2c84p;zqGA%Z`gi2)_OF#d3Q$A7ES^Q48^Y$%XE=V;4LR;#93$PI; z*fU1sl%)@~)o?0RK(Fg_g*!v>SEcI01prO5`nm2sNCu?mo?UC{UQ@tk!lzfRlt$m> zgq(A(3GbhpQRtl%%I(esa(}iuMM^JJvlS|(8y~d0LjJM4N}*+*rO2X_;vdT65|X9# z0+*_90<$#5ZAbZ$XO=~G4u1XI+FZbyrniJ0V1srTj~^SwSq(oyT`5me)5S?FTyXo8 zex3y?r4?;^JKvALz*8gt5_&2oumidCzL@*I|Kp9k`@gy#951sjvnpeY)&YI#ibk#< zE6=SEBqCNnx0b?WO)R<+McvqMafYu?aK<4bV`JZs*wJu+SkgUYJc%B{=NSoX(m zx{f{1?)Yfge%5ukH7tyJcl2}~y^D5|Kg3V)`(!W1`kRv=CRIJDiL3e&+e5 z{hF7rxWzLInVgIMm2iQh@>q=$+wSukrENm%}>-1 z*sqa0a#O+W$KLiCR)Ee}91s17)C8&@d}O1!-|3v1WWqwYaN>HQH;D8H$1f=0^0f^n zh5101Q^SMJFZ<8!YB_Eqam>TK@M6|GC>r3K17DRUauss_;$#n>4>xQ(b(FIky~=-)%5NMGax<5)VdgzEFBOPwSk$^;Q!r$SqC{i{#fXa1fnt^U`b1)Zx*$ z_fILCJ{7Y8>#H-(W)H;9y&ucg3G!O^+OPv?cnTU42dSkNSr@<@@qM(d+ zialLOJxBFcWZlfzi|c1>b{G>^u4AXJtoRimfoi_(3&P@{3QW?3uSdf?UwdMJn^^h% z(<`&`U+pox#6G5H&1A?tXk%EKkoI87!)hefUq--8Nbv{DYvefYZP7b+W#0exJEH62Z<{ zyc0e_bwuO9J1eK?2#Cv&ZoX$#jSCRD;GWxWFTcO+tOOcEe$;J+YBS0GIlRXd^gmWP zWcIx+9-ztHupLP91s_tA5nYI{DH^9_PhtXKRe|IhXPPO6UtH5-jjeGj=lD_DqbN>j z2KT@10+|Piu)z3gb={Iny%o zz1(M-6qO&Q0cqPSCPVo;52+(-)<9g#N5Q@{K2X0{kV}u=x_wjO2iX*P58xy65>H_*1HHj&lvnXwq zOt|Ix(-@)4kN5@~EvDA>>dFrnp9C29wA3VO`PFt&K}2c`scA=AdC17&zuMD(YcGF= z=KV)gb&M^g;H<1<$8X1bmNQ{z89n1IBsr<iIJ&s{w25b3FA)>7^!B73Gd>H>(z@ z5^mAZEvB+7A7ETgg`uI&3=Qp3d|syTVL5ZMU~Ep4{>kP%cK1lz{^E}(F54`hYgq0Q z0pswKvZDg(b&NW<$z0<}y!|YRm9CMF7gMi%4?7Y@K~dm}w6AkFO0g1izBk(H6hx;R zea*VCS*1e4w-zhsstGDZwpI4Trv=hd)+jD-*}AXJMzdODl1HR;p9>1ld5l-Tv6*M3 zP?57El;pJ#=`wKkEcGgaw0!3gtWSx1vrzu zKG1K3tc6`m(Y!=CD)UHJ9 z&UmBh@uZAznKR8nXf1YfykxeKO?Q0j%nYQiY1G*+j^_|WH-WT)*_>*UsO77>qrtXZiV7H;*Y-TOmvr6P5zlt+>vW`P)~STys#2pziIP5ktkx~s1UsLL6Oz>;p@f&^?BUk1W6zg zA#Z{5Uzt*C(dw!z(IG{j0 zrZ+|SmiM8J%>!8X(wHm~w3WHbEZ{v;<`T(J``H6d!bvF5^zP46>l^T-O8+gQNbj+l zcOD3X#}|iH#u|9)ws*_ieS`d^IeG${E`+HuY?MZfr63}kB=#$6gq8j+ zN}gsU*e+t_KKCiapZP{f`8ra4%P1mG5p$*+y13SJv2zGlDTk=cRK6}J0u~;2xA1Wn zt~o&T!8TSjl~p0FN=(ACWn@4GgctJv4RowKIt06rakhUaLp;addhTO6y$*vbzh_%; M357MTG$cg+2URvI(*OVf literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_name.png b/Telegram/Resources/icons/payments/payment_name.png new file mode 100644 index 0000000000000000000000000000000000000000..fdeb96631e221049b28551f4ff64c837c05a3664 GIT binary patch literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZp5{=IrU>7!twx zcFNt8tp+@8ua8GM6^Mox@;ZJmWYC{;$Z}6HgZ`HIc@G*danJsISn#03mfU-P%rc+N z`Fv>k#rv7_%8!>bl(VjjY}}+1d;fdhEVa~YbM|`gVNT1Nbu9n(v*f7jSCy-eUhCUx zJzdAPZ(8cd(%9cN=ch&%xAC0mcD%tnAt?KiMbF#~r(m%0 zO-B7%O^M@ylM2_Z()!yz|M^W};Zs4`#V7spolYLvp1b<)I&rz0I-@oghG5QJbK9CD z_Z!w_Y2WTW^y7x`ro&IQtuCJ-^};+uILW!5BwijTh&+I3RMRMtEa1<%Q~loCIHS>xqkov literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_name@2x.png b/Telegram/Resources/icons/payments/payment_name@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2bcacb37469370530d67d3ce316c4ecd6b91a693 GIT binary patch literal 884 zcmV-)1B?8LP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAu8-bqA3RCodHS--L|F%%CoR4N^> zpf#h{=_nLxZ==zu^m;Gg4YUd~I>rkq4f)P5!~L>x@80F)EO(PLlabw=zrUQEB&(#9 z($s>bamO1mq4CNpi-%*;c%!Hi-p>5 zx9W5{nT9^zVGKNZ6RyMJZ7>+<-EOCUBk+JX51(!}WP>l4%X%`I{1*4aIAkE30}!$R zyb}B!4aoj!0FcTqU9VR?pU-ut)6unB%{27!4r4CmupDGEfQs3}nm-lv&?T4&G5}CTx8pFgn-=^%K=8Pk0A2TSfnON_Xj@AFfE3T_Ja}z_uB@^z@W}u` zd%GQjS@T()Lrt&=y0Xf?z$XI$9dhg#OtH)A9Ezh&(3Ms8S-yA_Cd5RDs(C_htJO+8 zr#-m5uC6rMXUD`cV7*=^d>N0&iRZKjm)F&mCi}{m$odL?8w~y+M5$`X0vIGVxPxWw_GkwJD<<;&fF~I z3P5gAJz+@fAoPR}Z)J@DycPZrFz!7AaFLl#r)sm=m~Z}c_EDUM{|=xFI^9c>FZ{gR z1*+Amp3P?Za=GBZCqmE-8?Z&e`)YheaA>t!i3I(fQrPkc0v`a(O5OyUp0a=k0Ikc1 zwBPUddNdm8Mx()Y5;~v@I&H#ofNc*T@BpCVWcy_tt=H>5982hcP8S5&emekEp%VaL zwmdlXtq$n63AR1h;$?vQ_18z*{}dE-+vMYGG5an+4w1kAsZ@eWIr44K`$0%9bQ#TT z01k)4muGhW>!s(8ZNu(!%&-bcuh%ox85J6RazwQGw#Tjk9OOSimc0Z%Aqb8S5daQ) zMUagdP=s0J#1J!pgI*D2V+Isq7CAA*4B((w1lgDYMVLiS4F3S;0GY1s8sAa?0000< KMNUMnLSTY26Ld}h literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_name@3x.png b/Telegram/Resources/icons/payments/payment_name@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..23d2ee2435c84333aa996cc3f8660afe562304e9 GIT binary patch literal 1411 zcmZWpeK^wz9RK~cIonD%7CDq@8F>j;(@-`qnJx1cBj!i;z z?N-knu|i&oM|G&~TzT81TJd=4Y~q}o=eei*jXf(h|lfwZpFAjihN;ELi005w3 z5P)hJ-0X=VpQlVQ^uK&l*tDPF3jnY=il=x$EV9gmIP34!Dx=maphdBsd;@b;XXoXky>lJvhvTz?^6y`#*nNyS8I#pn zegFP_+11pic|D%+>=ysQ>LBcu}ShK2FMEh+L(6z@}e)ej=k+Vb*QqJx7xFE_UU+F9F^HSQ_LyJ>@=x6ias zO-xLzwzaiMu!fFO8iHjRwo{1#1|OD&hlj7o<#NeX`Litv!#Hz(@JlPgAR2Z0&Xo_D zf*9nAxj43@88&8tiHWn$ylTOWiNWDRIhARF0Lz(*U^Tpq{%sH~mC20z5IbJ9Gq3M^ z%>U;1bo5;CY%VLw0!Jp39q^R$rQ!lg$^`E=5+c2AP{KA7Hc{G33xj;Fe9RT}9bH^r z#ye|8RhzDce&7{WU}7RBX+MVqfI2$YZ&XRwQ7^OuudR+z^LprV!|~1{NSs#!H10u5-6aG`e-!$a)}B!;B}#9$_MSahQ9yDpDFYbPNxr4ZBi`1m zDio~GoT?mBIwdER%*mccK8_bxN}ap)eqk_Zz4WMnFXTTSUtOLVBYN=O4+mhK^sAEb zOB)|)LLdq${Cs!aFg_RS=;-+7YI*s%LUGO5)YPuy6Eev`>b|5>(KG$h($nAi`uU9y z4CHe%GMK_Yo}5#;$QQ(8^Yim-YPEX)pk#MvF*L=^7qraBCJoisn{O;F?S`Z^iu)05 zh@zrG-jgS^jm5=bg!b@8k#WjnANO4l`}PAljIRq#_J=@-UtgmB9u~SpgAHp2IJozr%4^UL1(64LE$dM2Nu_G2~GEhhu74a_>>$97w?|br_qjw2| zYFdmd^CnMh$6^q)8saE|e;&F0*^KhEGu5WCV&9xReM5?7v)TRwHVrooqv^dV1xmsQ zD(-U(>T1Pz==ACporrh*W)CYrRWRxiUuhwlJ?2l$?i=2L literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_phone.png b/Telegram/Resources/icons/payments/payment_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..87b185d8c6eba2ee0ccf073755c7f4597c336758 GIT binary patch literal 711 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZv>fz+~v@;usRa z`8MS2>WzT{@_VX(Mre46m2Fa7+Hr&}JJK^ULPPU^*VNYDy}P8v!py>Sbf!#Nq_ajt z{%Q3)%gb*yzkizZdEdL)y(>STxom0t`S;wJmWvq~QeLMns+IgC{$ch3Yi9lP2jpjl zPdNWvaiYh8tgTHaQ<&uCoBWo4Uf3VC=Y9Gip&c=L zyIdDvY>3fge_JNKB}#Y6<;+|4-;{L@=9nq-lx45Dnzi8aO9ip+#={RQ#Ag3#m6Yf| zo^m3{ugYrfq;0p~E}8$AgR{p?*+Q=WfOn#al;Tt`!DnfkE5)Keikk_qw*H#9|GxU( z+ix#C{>brHqN%6%#)=3XEqs|AUk6bng`* zS|OVYEPQ^M+Xwkniaq;m8Mi+C$>C1H9~*ttgpHbBlIW6fIGen}- zihT;XFMMx{*=%3kJ_(mLj?+wOn9 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_phone@2x.png b/Telegram/Resources/icons/payments/payment_phone@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d047fb5b4bff65dd84d02caae0aff967aa37d2fb GIT binary patch literal 1327 zcmV+~1Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuAnMp)JRCodHS=%dhK@^{Z2%(6C zM6M4!d7$Jjh43J+{sBqyprkx_^Fm6lL$qB7&r*rjmew0J68YNA~ykrT5v{8Ba0XdEs%`@YB%H0MF0Q z@L9RNy@l!NX~@dTGAxIOu2wvZ8(&^t{+ZpXwx_2jNJ&W%4Fux=tgNi4jMZuw92^u1 z0Lyzg(3k#lcXvlTJw0Ux-QC?JG&Gcihlfj#udlEC?fm?l1O^7mv`6~gCvI+ke=^WZO%n=rfz<4S*Ybi}OmnVFeDL4cv5A*LOP{QP{wGAU@hc<^D=)6>Hk+T7d> z{r&w+8xo_Vqr4R7#fJ|7B_$<{v9GVM42{J4`Z@#!1@Tgx7au+Vczb)p{r$bl=#K`v zRYFKe2rs31^5Fvj&dHIHkq>*BH{At5`FF^1{_+5V9{>df1&rZH93CD*R8$lXMeW6d z9{}jt`T2PUgwoPddnwL~FJAzPii#Kj78e(#?NG3=SAxeE05o)AVSxdmzP?^5P_#OK z0PsggSuhinl$0cz;gUXF5+c;=s?yrpN-8TW<@W9EZ9+jHmzS4v+gFw1;$o7JkU;$W z{K&z`m?b#-;n)zt-KV`D&%-l`lU_RGu5Qvqtj>&m|N2LKgGOiToNQ-r!Q zWo>N@Xud!)l=(o3@$qr+@bFM7E7kG%-P_yCbg!tWQ1ufE091x%f?#=hnNeJen46me zJYZ=V(bCew46UlF((J3n1KYV|geaqL6?!fpX=!Pst*wpFDPVI9-Yeog<40!6y7SxH z8`I?CqTdu<$WiHr#>L0S!|?DhC8aI8>SbkR|0VzkAPrw>r|9hLgwxa09|Q3C_y}QPVgD`w$|!Pm zb%mUq9B6NE2kR?0Eo5qIYxNorb~_7DqYM{Z*zkfiE-p^>;Dsw*$TT%Ik%55$-LixR zK(|#t7^r{m_<<0SyP5!q>>WqwW=4P`P!?H+837`D#}T@j5#R`vMV4VkfXLo)gl=X8 lI09vnWtb5lvUePz+aGJ+c$OHjO3eTO002ovPDHLkV1k6cRF?n% literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_phone@3x.png b/Telegram/Resources/icons/payments/payment_phone@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ff201d3e80b5a34d04cb0e33f0b6d464a3c43608 GIT binary patch literal 2090 zcmbtVdpOf=AOCGO%z0*njG9Bpsgbj1wAswLn6oYA5Rx)2q4p>_w4eo{rmmnbALYfeSg2->;B%?eP5qEKOcgkoR%B_ z0E!+&+(8Lbe?nGTGJdI`+erwNa**H#)N^;vNG7=GV2>DYZ@@&NWdTsmF#z&2L;@`d z06>Zf0;D7i`nkpg|7VxVl=`p!8F&{%@c{stQV*PKz!}g&cmgrNRSiYKlS}{UzGjId zHQu~w1ih**{k5U^7PH{d3PDe-g7%vZf{01RCS|phDP{N!p6I%&H@)kyrYbq!{cYbK!dA zH|mmfH1_`QX8$gbxwmYqx5VhO@uAzfk57!}cXYT2gnZw7H;ZOds~9#3gSluzVQ!jm z>WS&r+>nJ}<*TP)b0cM4!0SZ_rr)AZ&JCIgeypFMj}Y6ikJU!6)Yk4vTb=fYQAwts zx5PhAb>}0S%HO~gtR3cMbdlwyrNNz_&*&}B8I+0J$nT@rjP3lw^aU5~H zsVS4Ft}vb~$ga;n`~4l?%RJsuxUR0nJ`ARN1$qDk_Tgseek<)m0dODaeci{A<8*4vLhozb4gWQLxZ3t3fATC zW!BdAWY@gZylN1gkWiPCIQ_cu$jjaEj93_>yl!)CVbm{CJa)rA#qf&@$4hbCULIRx zW@dH{WNTzcU#u#~%gg&PxO5+?2MTLSw3?g~YV~F{j1#5D)4<6U8(;f1Zg7t9YCOve zG}yfX=rY;ZRz1k7NHFOFV?Z_SV_sDdp&4=QRYrjGPTHceZPMRGaRzASSM5|~w`iR7B`sR00vZSke9ZqWpM4 zXCOwX#j4NNai9?^UncKP#!^cT1;@b|j43|9zk%{J{v?=k=tk|!8z!`pcb z@$t~>EtganPOGvUx5)r#lJ^QSnT&M0>x><4r8hJXcnhPCzs?V{4Vs_zNM!%)YA2Vs zD=U#<@5PId8_bDX7$M>w0|$lCQe3J%0kZo3q@Kb;B_s;PG7YLHeoUC(g&F5;Nx{-f zUL%JhPkd?_*sT5WU2=?(*h`)Yy)I(bz1EZ@B3){{U|hmwbhts5zyFj;@`o3^*_Rx89*!GV03Ka(%q zuf0FfaxZv$1^c*PI_#?b#2A;+>r$4Bu@BLZ z>w$Z2q0tuX$(L7L4Wyd9JVfa_h8$f`{h6wWPi<*zEA82YEDOJ@iL0-aw#|E>EsF*3-^r`* zwd_c)tgJL^$DG>~?NUZL#S!{#R}}J)RsdH}R9dQ^T&7?P<5pNb+u{8=S5CR$S)i3D zN8LX#(112tm*qbV_rzLqR1lG$M@xLbYf4i8ZfEDOGpwB`yUg&yQ Q^wXz3@IJVDH)__u0F;ZaD*ylh literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_shipping.png b/Telegram/Resources/icons/payments/payment_shipping.png new file mode 100644 index 0000000000000000000000000000000000000000..aa618f5fb67dc8da2f5e9844942042918dd3d266 GIT binary patch literal 526 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZp5{R^jR57!twx zHq>#Qi-SO}ib~!?hP;Ht-o4TmjlI$xvpbr1G`02$co#DLUHz-y<$H;=vap%fOT*CE z(D#*57daF~uH`Pf-}Z;8j>Cq_hD$@#z<2VKO21=;DpM{wXGe-%GU4rf{A2ZYY4(e^ znFyh zN@l68di_gKXuoR1lUv(TRcy}hZg-fL+T##={mP~_{wj6r8**=Nd2QvkNXL6s<}JC- z`@#o~KiH)=_xa3;9+$sVF?lF;DY-FR4Umm_yOmM=-hxT24t8~qz267RKUIk-+$Joz zT*=S9r?bq&e~ojz7XM|wx)fo-Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAu9Xh}ptRCodHSut)ZF%UHrPy{KV zjFc#6NP!e2L=}p3T!DfkP;rEGbXiR4EPd=c>*w>0X_p_&VVn0m?waL z2>KH6B@lB4jxK>jBEhQFDtmi-WApi(an7yJ<#Ng1-``oI(O}ot*Cz`V8X}}AjN$!$ zU-*2!u~;m4xm+gMY3LWtc@g=uJHMq_WCSl13bsk05g^ijYtOHI4u`|vB|y}?;&`vu z<2N@qwvF20U0z=DTCJw&!{+|}-ljfU28aiqV$i9am+SZC+K>ilUWHQRr z`1A8q*`H$5?RFK|$HzxyA9X`wQwfFG2<0cAcXxM++SO{sFD@>u=p|=BRJM<~|KsC> zjmP8Nt*KOs6^q5)KFY!g0^s25>#O2trBYG$Q8uJhQgZQ3B_N#RrBX>zyDo;KKte8;ll|x7Wy#d(om>JyVj2&jQISPw=(h%R zi`3t#hi%Cu?cI(hy;JpXENleB8Ucug+#^Jv5dA`&9g`)t@qP-~t{#L?=vpbdzaijZ zJwo&kA!C9M848}x9#cKva^X(PfI)>KQbs*OkN{a3OSh)t1?|ychMYwsfYKv`Y=|+O zig$JbrqgLqr?TJn2+=CTI_jX{!L~4hHP>kw&}y}Q^hxd!VjXo*@L-$9!LTzL0WfGb zn;rCvV(>u_>4GU9Y?5UOM9>HTFcg7pT{%!vR`T=UG7Jwkz?LJOM|RK&knf>BwB&nr zYuk7_9f7lG1W>=W0?PO5(>7k1K5;!70l1&?;$ouDHeQI%=mbCkB8T+FUuXFD2or0Z z;aZ3hd)IB_9nw$!H7J1ot6}ZOkgKeC0$h!pExUaN*kaOE9G?NMM$VSqJ_Bqq=_-!T s09PYt%Wj_mwwQDk$7g`6k+Ws@A9kxCwv1J*{Qv*}07*qoM6N<$f=M62^Z)<= literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/payments/payment_shipping@3x.png b/Telegram/Resources/icons/payments/payment_shipping@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8b08421d09b50c2947c5031b96039ece6917f8ca GIT binary patch literal 1469 zcmb7^doM256;Vh} zApj8N5I`s#+PW)O`g=+#NBmQ71*;B5k^n%N5#5{vFG8Q#N4=H^TGtzDN{uJuDoQyI zD5fFn*Fr+&SMF{iVOVH=#^i)t6Y0KTZuMjd2|v?tBMT5HzZWg5%XTxS#S=cZv9zhS zSjq(bP_d*$Qd|&K0Bazxj9SNet7{b}xr_M_^!$i6G6PU9WQ0cSCjgw&b1Q3YBT!^O z`LPhK0bZ-)e)B@9fS7GX^Bg54Xh6LF><=krOh7ToRRDUv1RB}{|NUI>4MNwzK<$vd zeP(WM?(F=$DcKY5n3y<(KHUA%S-J(?p3x!gBqn z2AuQnl)(4tX1w*Wxg^a<)gN zWBW`Uyx%T{THa=^oGz)VvN-*Ay>xhV^mgzRUU~Pykjw<%oSfL(vSi9Q9FBq#2!$22=5GSu0Wu1U#U57~8XD>_oKyi*Uq%XlI<8`G zZ(rTez!k1B>AZcyp7 zd#!b*_a-~^5(}al3EaASy~dGCb4B5COuXv#%2wWI8q0TO#gH%_#Km=UcA6)BQ3VHT z9>V9wi7pjkt3crPs_`WAXN|{6RQM6=QI{B>ZN%@UhAGV}Lg`wTk1eOsO{{{ZG8dhe z<2d{$1|E=neFKuA?n+|$5TkO~y>8^K!RABgS#xvIuTI?RxHaint=4|t3&BE%V!e(U za)R~E#CcsedQL@STNjOw{qBj%SSoxrXj8;ytEyu#^aLJ%VQPi?sKrBU=y(0@vHB`Ter^*VFTCsFv1{+!&O0qK4orI{=O5J^ zHy*V;Oc1+TWi~B6Fi>EGOHhFEe?jEG81x!!0%?SPhwACVYg_0fx|7`MTqsxn0)+#N AFaQ7m literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 11bb8041c..6be8d6bd0 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1865,24 +1865,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_pay_amount" = "Pay {amount}"; "lng_payments_payment_method" = "Payment Method"; "lng_payments_new_card" = "New Card..."; -"lng_payments_payment_method_ph" = "Enter your card details"; -"lng_payments_shipping_address" = "Shipping Information"; -"lng_payments_shipping_address_ph" = "Enter your shipping information"; +"lng_payments_shipping_address" = "Shipping Address"; +"lng_payments_receiver_information" = "Receiver"; +"lng_payments_address_street1" = "Address 1"; +"lng_payments_address_street2" = "Address 2"; +"lng_payments_address_city" = "City"; +"lng_payments_address_state" = "State"; +"lng_payments_address_country" = "Country"; +"lng_payments_address_postcode" = "Postcode"; + "lng_payments_shipping_method" = "Shipping Method"; -"lng_payments_shipping_method_ph" = "Choose your shipping method"; "lng_payments_info_name" = "Name"; -"lng_payments_info_name_ph" = "Enter your name"; "lng_payments_info_email" = "Email"; -"lng_payments_info_email_ph" = "Enter your email"; "lng_payments_info_phone" = "Phone"; -"lng_payments_info_phone_ph" = "Enter your phone number"; "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"; -"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_card_title" = "New Card"; +"lng_payments_card_number" = "Card Number"; +"lng_payments_card_holder" = "Cardholder name"; +"lng_payments_billing_address" = "Billing Information"; +"lng_payments_billing_country" = "Country"; +"lng_payments_billing_zip_code" = "Zip Code"; "lng_payments_save_payment_about" = "You can save your payment information for future use."; "lng_payments_save_information" = "Save Information"; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index d6ae27bb5..5b7287066 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_domain.h" #include "history/history_item.h" #include "history/history.h" +#include "data/data_user.h" // UserData::isBot. #include "core/local_url_handlers.h" // TryConvertUrlToLocal. #include "core/file_utilities.h" // File::OpenUrl. #include "apiwrap.h" @@ -105,6 +106,8 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { if (!_initialSilentValidation) { showForm(); } + }, [&](const ThumbnailUpdated &data) { + _panel->updateFormThumbnail(data.thumbnail); }, [&](const ValidateFinished &) { if (_initialSilentValidation) { _initialSilentValidation = false; @@ -114,16 +117,16 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _submitState = SubmitState::Validated; panelSubmit(); } - }, [&](const PaymentMethodUpdate&) { + }, [&](const PaymentMethodUpdate &) { showForm(); - }, [&](const VerificationNeeded &info) { - if (!_panel->showWebview(info.url, false)) { - File::OpenUrl(info.url); + }, [&](const VerificationNeeded &data) { + if (!_panel->showWebview(data.url, false)) { + File::OpenUrl(data.url); panelCloseSure(); } - }, [&](const PaymentFinished &result) { + }, [&](const PaymentFinished &data) { const auto weak = base::make_weak(this); - _session->api().applyUpdates(result.updates); + _session->api().applyUpdates(data.updates); if (weak) { panelCloseSure(); } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index b6ad2c95e..6114bcb65 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -9,10 +9,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "data/data_session.h" -#include "apiwrap.h" +#include "data/data_media_types.h" +#include "data/data_user.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_file_origin.h" +#include "history/history_item.h" #include "stripe/stripe_api_client.h" #include "stripe/stripe_error.h" #include "stripe/stripe_token.h" +#include "ui/image/image.h" +#include "apiwrap.h" +#include "styles/style_payments.h" // paymentsThumbnailSize. #include #include @@ -82,15 +90,110 @@ namespace { Form::Form(not_null session, FullMsgId itemId) : _session(session) , _api(&_session->mtp()) -, _msgId(itemId.msg) { +, _msgId(itemId) { + fillInvoiceFromMessage(); requestForm(); } Form::~Form() = default; +void Form::fillInvoiceFromMessage() { + if (const auto item = _session->data().message(_msgId)) { + if (const auto media = item->media()) { + if (const auto invoice = media->invoice()) { + _invoice.cover = Ui::Cover{ + .title = invoice->title, + .description = invoice->description, + }; + if (const auto photo = invoice->photo) { + loadThumbnail(photo); + } + } + } + } +} + +void Form::loadThumbnail(not_null photo) { + Expects(!_thumbnailLoadProcess); + + auto view = photo->createMediaView(); + if (auto good = prepareGoodThumbnail(view); !good.isNull()) { + _invoice.cover.thumbnail = std::move(good); + return; + } + _thumbnailLoadProcess = std::make_unique(); + if (auto blurred = prepareBlurredThumbnail(view); !blurred.isNull()) { + _invoice.cover.thumbnail = std::move(blurred); + _thumbnailLoadProcess->blurredSet = true; + } else { + _invoice.cover.thumbnail = prepareEmptyThumbnail(); + } + _thumbnailLoadProcess->view = std::move(view); + photo->load(Data::PhotoSize::Thumbnail, _msgId); + _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + const auto &view = _thumbnailLoadProcess->view; + if (auto good = prepareGoodThumbnail(view); !good.isNull()) { + _invoice.cover.thumbnail = std::move(good); + _thumbnailLoadProcess = nullptr; + } else if (_thumbnailLoadProcess->blurredSet) { + return; + } else if (auto blurred = prepareBlurredThumbnail(view) + ; !blurred.isNull()) { + _invoice.cover.thumbnail = std::move(blurred); + _thumbnailLoadProcess->blurredSet = true; + } else { + return; + } + _updates.fire(ThumbnailUpdated{ _invoice.cover.thumbnail }); + }, _thumbnailLoadProcess->lifetime); +} + +QImage Form::prepareGoodThumbnail( + const std::shared_ptr &view) const { + using Size = Data::PhotoSize; + if (const auto large = view->image(Size::Large)) { + return prepareThumbnail(large); + } else if (const auto thumbnail = view->image(Size::Thumbnail)) { + return prepareThumbnail(thumbnail); + } + return QImage(); +} + +QImage Form::prepareBlurredThumbnail( + const std::shared_ptr &view) const { + if (const auto small = view->image(Data::PhotoSize::Small)) { + return prepareThumbnail(small, true); + } else if (const auto blurred = view->thumbnailInline()) { + return prepareThumbnail(blurred, true); + } + return QImage(); +} + +QImage Form::prepareThumbnail( + not_null image, + bool blurred) const { + auto result = image->original().scaled( + st::paymentsThumbnailSize * cIntRetinaFactor(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + Images::prepareRound(result, ImageRoundRadius::Large); + result.setDevicePixelRatio(cRetinaFactor()); + return result; +} + +QImage Form::prepareEmptyThumbnail() const { + auto result = QImage( + st::paymentsThumbnailSize * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(cRetinaFactor()); + result.fill(Qt::transparent); + return result; +} + void Form::requestForm() { _api.request(MTPpayments_GetPaymentForm( - MTP_int(_msgId) + MTP_int(_msgId.msg) )).done([=](const MTPpayments_PaymentForm &result) { result.match([&](const auto &data) { processForm(data); @@ -123,6 +226,8 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { void Form::processInvoice(const MTPDinvoice &data) { _invoice = Ui::Invoice{ + .cover = std::move(_invoice.cover), + .prices = ParsePrices(data.vprices()), .currency = qs(data.vcurrency()), @@ -154,6 +259,11 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { .canSaveCredentials = data.is_can_save_credentials(), .passwordMissing = data.is_password_missing(), }; + if (_details.botId) { + if (const auto bot = _session->data().userLoaded(_details.botId)) { + _invoice.cover.seller = bot->name; + } + } } void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { @@ -240,7 +350,7 @@ void Form::submit() { | (_shippingOptions.selectedId.isEmpty() ? Flag(0) : Flag::f_shipping_option_id)), - MTP_int(_msgId), + MTP_int(_msgId.msg), MTP_string(_requestedInformationId), MTP_string(_shippingOptions.selectedId), MTP_inputPaymentCredentials( @@ -267,7 +377,7 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _validatedInformation = information; _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( MTP_flags(0), // #TODO payments save information - MTP_int(_msgId), + MTP_int(_msgId.msg), Serialize(information) )).done([=](const MTPpayments_ValidatedRequestedInfo &result) { _validateRequestId = 0; diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 7fbbe6c2a..803dd4b3f 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "mtproto/sender.h" +class Image; + namespace Stripe { class APIClient; } // namespace Stripe @@ -19,6 +21,10 @@ namespace Main { class Session; } // namespace Main +namespace Data { +class PhotoMedia; +} // namespace Data + namespace Payments { struct FormDetails { @@ -38,6 +44,12 @@ struct FormDetails { } }; +struct ThumbnailLoadProcess { + std::shared_ptr view; + bool blurredSet = false; + rpl::lifetime lifetime; +}; + struct SavedCredentials { QString id; QString title; @@ -88,6 +100,9 @@ struct PaymentMethod { }; struct FormReady {}; +struct ThumbnailUpdated { + QImage thumbnail; +}; struct ValidateFinished {}; struct PaymentMethodUpdate {}; struct VerificationNeeded { @@ -109,6 +124,7 @@ struct Error { struct FormUpdate : std::variant< FormReady, + ThumbnailUpdated, ValidateFinished, PaymentMethodUpdate, VerificationNeeded, @@ -149,6 +165,18 @@ public: void submit(); private: + void fillInvoiceFromMessage(); + + void loadThumbnail(not_null photo); + [[nodiscard]] QImage prepareGoodThumbnail( + const std::shared_ptr &view) const; + [[nodiscard]] QImage prepareBlurredThumbnail( + const std::shared_ptr &view) const; + [[nodiscard]] QImage prepareThumbnail( + not_null image, + bool blurred = false) const; + [[nodiscard]] QImage prepareEmptyThumbnail() const; + void requestForm(); void processForm(const MTPDpayments_paymentForm &data); void processInvoice(const MTPDinvoice &data); @@ -167,9 +195,10 @@ private: const not_null _session; MTP::Sender _api; - MsgId _msgId = 0; + FullMsgId _msgId; Ui::Invoice _invoice; + std::unique_ptr _thumbnailLoadProcess; FormDetails _details; Ui::RequestedInformation _savedInformation; PaymentMethod _paymentMethod; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index d331d3033..811f70160 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -7,12 +7,52 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ using "ui/basic.style"; -using "passport/passport.style"; +using "info/info.style"; -paymentsFormPricePadding: margins(22px, 7px, 22px, 6px); -paymentsPanelSubmit: RoundButton(passportPasswordSubmit) { +paymentsPanelSubmit: RoundButton(defaultActiveButton) { width: 0px; height: 49px; padding: margins(0px, -3px, 0px, 0px); textTop: 16px; } + +paymentsCoverPadding: margins(26px, 0px, 26px, 13px); +paymentsDescription: FlatLabel(defaultFlatLabel) { + minWidth: 160px; + textFg: windowFg; +} +paymentsTitle: FlatLabel(paymentsDescription) { + style: semiboldTextStyle; +} +paymentsSeller: FlatLabel(paymentsDescription) { + textFg: windowSubTextFg; +} +paymentsPriceLabel: paymentsDescription; +paymentsPriceAmount: defaultFlatLabel; +paymentsFullPriceLabel: paymentsTitle; +paymentsFullPriceAmount: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; +} + +paymentsTitleTop: 0px; +paymentsDescriptionTop: 3px; +paymentsSellerTop: 4px; + +paymentsThumbnailSize: size(80px, 80px); +paymentsThumbnailSkip: 18px; + +paymentsPricesTopSkip: 12px; +paymentsPricesBottomSkip: 13px; +paymentsPricePadding: margins(28px, 6px, 28px, 5px); + +paymentsSectionsTopSkip: 11px; +paymentsSectionButton: SettingsButton(infoProfileButton) { + padding: margins(68px, 11px, 14px, 9px); +} + +paymentsIconPaymentMethod: icon {{ "payments/payment_card", menuIconFg }}; +paymentsIconShippingAddress: icon {{ "payments/payment_address", menuIconFg }}; +paymentsIconName: icon {{ "payments/payment_name", menuIconFg }}; +paymentsIconEmail: icon {{ "payments/payment_email", menuIconFg }}; +paymentsIconPhone: icon {{ "payments/payment_phone", menuIconFg }}; +paymentsIconShippingMethod: icon {{ "payments/payment_shipping", menuIconFg }}; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index dfcee629e..0f63805be 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_form_summary.h" #include "payments/ui/payments_panel_delegate.h" -#include "passport/ui/passport_form_row.h" +#include "settings/settings_common.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -22,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Payments::Ui { using namespace ::Ui; -using namespace Passport::Ui; class PanelDelegate; @@ -45,16 +44,24 @@ FormSummary::FormSummary( this, tr::lng_payments_pay_amount( lt_amount, - rpl::single(computeTotalAmount())), + rpl::single(formatAmount(computeTotalAmount()))), st::paymentsPanelSubmit) { setupControls(); } -QString FormSummary::computeAmount(int64 amount) const { - return FillAmountAndCurrency(amount, _invoice.currency); +void FormSummary::updateThumbnail(const QImage &thumbnail) { + _invoice.cover.thumbnail = thumbnail; + _thumbnails.fire_copy(thumbnail); } -QString FormSummary::computeTotalAmount() const { +QString FormSummary::formatAmount(int64 amount) const { + const auto base = FillAmountAndCurrency( + std::abs(amount), + _invoice.currency); + return (amount > 0) ? base : (QString::fromUtf8("\xe2\x88\x92") + base); +} + +int64 FormSummary::computeTotalAmount() const { const auto total = ranges::accumulate( _invoice.prices, int64(0), @@ -71,7 +78,7 @@ QString FormSummary::computeTotalAmount() const { std::plus<>(), &LabeledPrice::price) : int64(0); - return computeAmount(total + shipping); + return total + shipping; } void FormSummary::setupControls() { @@ -92,22 +99,125 @@ void FormSummary::setupControls() { _1 + _2 < _3)); } -not_null FormSummary::setupContent() { - const auto inner = _scroll->setOwnedWidget( - object_ptr(this)); +void FormSummary::setupCover(not_null layout) { + struct State { + QImage thumbnail; + FlatLabel *title = nullptr; + FlatLabel *description = nullptr; + FlatLabel *seller = nullptr; + }; - _scroll->widthValue( - ) | rpl::start_with_next([=](int width) { - inner->resizeToWidth(width); - }, inner->lifetime()); + const auto cover = layout->add(object_ptr(layout)); + const auto state = cover->lifetime().make_state(); + state->title = CreateChild( + cover, + _invoice.cover.title, + st::paymentsTitle); + state->description = CreateChild( + cover, + _invoice.cover.description, + st::paymentsDescription); + state->seller = CreateChild( + cover, + _invoice.cover.seller, + st::paymentsSeller); + cover->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + if (state->thumbnail.isNull()) { + return; + } + const auto &padding = st::paymentsCoverPadding; + const auto thumbnailSkip = st::paymentsThumbnailSize.width() + + st::paymentsThumbnailSkip; + const auto left = padding.left(); + const auto top = padding.top(); + const auto rect = QRect( + QPoint(left, top), + state->thumbnail.size() / state->thumbnail.devicePixelRatio()); + if (rect.intersects(clip)) { + QPainter(cover).drawImage(rect, state->thumbnail); + } + }, cover->lifetime()); + rpl::combine( + cover->widthValue(), + _thumbnails.events_starting_with_copy(_invoice.cover.thumbnail) + ) | rpl::start_with_next([=](int width, QImage &&thumbnail) { + const auto &padding = st::paymentsCoverPadding; + const auto thumbnailSkip = st::paymentsThumbnailSize.width() + + st::paymentsThumbnailSkip; + const auto left = padding.left() + + (thumbnail.isNull() ? 0 : thumbnailSkip); + const auto available = width + - padding.left() + - padding.right() + - (thumbnail.isNull() ? 0 : thumbnailSkip); + state->title->resizeToNaturalWidth(available); + state->title->moveToLeft( + left, + padding.top() + st::paymentsTitleTop); + state->description->resizeToNaturalWidth(available); + state->description->moveToLeft( + left, + (state->title->y() + + state->title->height() + + st::paymentsDescriptionTop)); + state->seller->resizeToNaturalWidth(available); + state->seller->moveToLeft( + left, + (state->description->y() + + state->description->height() + + st::paymentsSellerTop)); + const auto thumbnailHeight = padding.top() + + (thumbnail.isNull() + ? 0 + : int(thumbnail.height() / thumbnail.devicePixelRatio())) + + padding.bottom(); + const auto height = state->seller->y() + + state->seller->height() + + padding.bottom(); + cover->resize(width, std::max(thumbnailHeight, height)); + state->thumbnail = std::move(thumbnail); + cover->update(); + }, cover->lifetime()); +} +void FormSummary::setupPrices(not_null layout) { + Settings::AddSkip(layout, st::paymentsPricesTopSkip); + const auto add = [&]( + const QString &label, + int64 amount, + bool full = false) { + const auto &st = full + ? st::paymentsFullPriceAmount + : st::paymentsPriceAmount; + const auto right = CreateChild( + layout.get(), + formatAmount(amount), + st); + const auto &padding = st::paymentsPricePadding; + const auto left = layout->add( + object_ptr( + layout, + label, + (full + ? st::paymentsFullPriceLabel + : st::paymentsPriceLabel)), + style::margins( + padding.left(), + padding.top(), + (padding.right() + + right->naturalWidth() + + 2 * st.style.font->spacew), + padding.bottom())); + rpl::combine( + left->topValue(), + layout->widthValue() + ) | rpl::start_with_next([=](int top, int width) { + right->moveToRight(st::paymentsPricePadding.right(), top, width); + }, right->lifetime()); + }; for (const auto &price : _invoice.prices) { - inner->add( - object_ptr( - inner, - price.label + ": " + computeAmount(price.price), - st::passportFormPolicy), - st::paymentsFormPricePadding); + add(price.label, price.price); } const auto selected = ranges::find( _options.list, @@ -115,44 +225,35 @@ not_null FormSummary::setupContent() { &ShippingOption::id); if (selected != end(_options.list)) { for (const auto &price : selected->prices) { - inner->add( - object_ptr( - inner, - price.label + ": " + computeAmount(price.price), - st::passportFormPolicy), - st::paymentsFormPricePadding); + add(price.label, price.price); } } - inner->add( - object_ptr( - inner, - "Total: " + computeTotalAmount(), - st::passportFormHeader), - st::passportFormHeaderPadding); + add(tr::lng_payments_total_label(tr::now), computeTotalAmount(), true); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); +} - inner->add( - object_ptr( - inner, - st::passportFormDividerHeight), - { 0, 0, 0, st::passportFormHeaderPadding.top() }); +void FormSummary::setupSections(not_null layout) { + Settings::AddSkip(layout, st::paymentsSectionsTopSkip); - const auto method = inner->add(object_ptr(inner)); - method->addClickHandler([=] { - _delegate->panelEditPaymentMethod(); - }); - method->updateContent( - tr::lng_payments_payment_method(tr::now), - (_method.ready - ? _method.title - : tr::lng_payments_payment_method_ph(tr::now)), - _method.ready, - false, - anim::type::instant); + const auto add = [&]( + rpl::producer title, + const QString &label, + const style::icon *icon, + Fn handler) { + Settings::AddButtonWithLabel( + layout, + std::move(title), + rpl::single(label), + st::paymentsSectionButton, + icon + )->addClickHandler(std::move(handler)); + }; + add( + tr::lng_payments_payment_method(), + _method.title, + &st::paymentsIconPaymentMethod, + [=] { _delegate->panelEditPaymentMethod(); }); if (_invoice.isShippingAddressRequested) { - const auto info = inner->add(object_ptr(inner)); - info->addClickHandler([=] { - _delegate->panelEditShippingInformation(); - }); auto list = QStringList(); const auto push = [&](const QString &value) { if (!value.isEmpty()) { @@ -165,65 +266,61 @@ not_null FormSummary::setupContent() { push(_information.shippingAddress.state); push(_information.shippingAddress.countryIso2); push(_information.shippingAddress.postcode); - info->updateContent( - tr::lng_payments_shipping_address(tr::now), - (list.isEmpty() - ? tr::lng_payments_shipping_address_ph(tr::now) - : list.join(", ")), - !list.isEmpty(), - false, - anim::type::instant); + add( + tr::lng_payments_shipping_address(), + list.join(", "), + &st::paymentsIconShippingAddress, + [=] { _delegate->panelEditShippingInformation(); }); } if (!_options.list.empty()) { - const auto options = inner->add(object_ptr(inner)); - options->addClickHandler([=] { - _delegate->panelChooseShippingOption(); - }); - options->updateContent( - tr::lng_payments_shipping_method(tr::now), - (selected != end(_options.list) - ? selected->title - : tr::lng_payments_shipping_method_ph(tr::now)), - (selected != end(_options.list)), - false, - anim::type::instant); + const auto selected = ranges::find( + _options.list, + _options.selectedId, + &ShippingOption::id); + add( + tr::lng_payments_shipping_method(), + (selected != end(_options.list)) ? selected->title : QString(), + &st::paymentsIconShippingMethod, + [=] { _delegate->panelChooseShippingOption(); }); } if (_invoice.isNameRequested) { - const auto name = inner->add(object_ptr(inner)); - name->addClickHandler([=] { _delegate->panelEditName(); }); - name->updateContent( - tr::lng_payments_info_name(tr::now), - (_information.name.isEmpty() - ? tr::lng_payments_info_name_ph(tr::now) - : _information.name), - !_information.name.isEmpty(), - false, - anim::type::instant); + add( + tr::lng_payments_info_name(), + _information.name, + &st::paymentsIconName, + [=] { _delegate->panelEditName(); }); } if (_invoice.isEmailRequested) { - const auto email = inner->add(object_ptr(inner)); - email->addClickHandler([=] { _delegate->panelEditEmail(); }); - email->updateContent( - tr::lng_payments_info_email(tr::now), - (_information.email.isEmpty() - ? tr::lng_payments_info_email_ph(tr::now) - : _information.email), - !_information.email.isEmpty(), - false, - anim::type::instant); + add( + tr::lng_payments_info_email(), + _information.email, + &st::paymentsIconEmail, + [=] { _delegate->panelEditEmail(); }); } if (_invoice.isPhoneRequested) { - const auto phone = inner->add(object_ptr(inner)); - phone->addClickHandler([=] { _delegate->panelEditPhone(); }); - phone->updateContent( - tr::lng_payments_info_phone(tr::now), - (_information.phone.isEmpty() - ? tr::lng_payments_info_phone_ph(tr::now) - : _information.phone), - !_information.phone.isEmpty(), - false, - anim::type::instant); + add( + tr::lng_payments_info_phone(), + _information.phone, + &st::paymentsIconPhone, + [=] { _delegate->panelEditPhone(); }); } + Settings::AddSkip(layout, st::paymentsSectionsTopSkip); +} + +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()); + + setupCover(inner); + Settings::AddDivider(inner); + setupPrices(inner); + Settings::AddDivider(inner); + setupSections(inner); return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 38f30ff0f..8ce6bcd4c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -15,6 +15,7 @@ namespace Ui { class ScrollArea; class FadeShadow; class RoundButton; +class VerticalLayout; } // namespace Ui namespace Payments::Ui { @@ -33,15 +34,20 @@ public: const ShippingOptions &options, not_null delegate); + void updateThumbnail(const QImage &thumbnail); + private: void resizeEvent(QResizeEvent *e) override; void setupControls(); [[nodiscard]] not_null setupContent(); + void setupCover(not_null layout); + void setupPrices(not_null layout); + void setupSections(not_null layout); void updateControlsGeometry(); - [[nodiscard]] QString computeAmount(int64 amount) const; - [[nodiscard]] QString computeTotalAmount() const; + [[nodiscard]] QString formatAmount(int64 amount) const; + [[nodiscard]] int64 computeTotalAmount() const; const not_null _delegate; Invoice _invoice; @@ -52,6 +58,7 @@ private: object_ptr _topShadow; object_ptr _bottomShadow; object_ptr _submit; + rpl::event_stream _thumbnails; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 97c652e12..1ae7054ec 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -52,17 +52,24 @@ void Panel::showForm( const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options) { - _widget->showInner( - base::make_unique_q( - _widget.get(), - invoice, - current, - method, - options, - _delegate)); + auto form = base::make_unique_q( + _widget.get(), + invoice, + current, + method, + options, + _delegate); + _weakFormSummary = form.get(); + _widget->showInner(std::move(form)); _widget->setBackAllowed(false); } +void Panel::updateFormThumbnail(const QImage &thumbnail) { + if (_weakFormSummary) { + _weakFormSummary->updateThumbnail(thumbnail); + } +} + void Panel::showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index a8a694a89..e07703ba9 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -28,6 +28,7 @@ struct RequestedInformation; struct ShippingOptions; enum class InformationField; enum class CardField; +class FormSummary; class EditInformation; class EditCard; struct PaymentMethodDetails; @@ -45,6 +46,7 @@ public: const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options); + void updateFormThumbnail(const QImage &thumbnail); void showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, @@ -78,6 +80,7 @@ private: const not_null _delegate; std::unique_ptr _widget; std::unique_ptr _webview; + QPointer _weakFormSummary; QPointer _weakEditInformation; QPointer _weakEditCard; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 7c3964c29..03f37aa4e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -14,7 +14,16 @@ struct LabeledPrice { int64 price = 0; }; +struct Cover { + QString title; + QString description; + QString seller; + QImage thumbnail; +}; + struct Invoice { + Cover cover; + std::vector prices; QString currency; From 47fdef1e38478a5cb203e19655d0d2a501842ea8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Mar 2021 19:23:12 +0400 Subject: [PATCH 020/127] Improve checkout information / card page design. --- Telegram/Resources/langs/lang.strings | 3 +- .../payments/payments_checkout_process.cpp | 26 +- .../payments/payments_checkout_process.h | 2 + .../SourceFiles/payments/ui/payments.style | 11 + .../payments/ui/payments_edit_card.cpp | 199 ++++++-------- .../payments/ui/payments_edit_card.h | 24 +- .../payments/ui/payments_edit_information.cpp | 251 +++++++----------- .../payments/ui/payments_edit_information.h | 33 ++- .../payments/ui/payments_field.cpp | 140 ++++++++++ .../SourceFiles/payments/ui/payments_field.h | 62 +++++ .../payments/ui/payments_form_summary.cpp | 15 +- .../payments/ui/payments_panel.cpp | 19 +- Telegram/cmake/td_ui.cmake | 2 + 13 files changed, 468 insertions(+), 319 deletions(-) create mode 100644 Telegram/SourceFiles/payments/ui/payments_field.cpp create mode 100644 Telegram/SourceFiles/payments/ui/payments_field.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6be8d6bd0..3f49c3076 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1866,7 +1866,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_payment_method" = "Payment Method"; "lng_payments_new_card" = "New Card..."; "lng_payments_shipping_address" = "Shipping Address"; -"lng_payments_receiver_information" = "Receiver"; "lng_payments_address_street1" = "Address 1"; "lng_payments_address_street2" = "Address 2"; "lng_payments_address_city" = "City"; @@ -1878,7 +1877,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_info_name" = "Name"; "lng_payments_info_email" = "Email"; "lng_payments_info_phone" = "Phone"; -"lng_payments_shipping_address_title" = "Shipping Address"; +"lng_payments_shipping_address_title" = "Shipping Information"; "lng_payments_save_shipping_about" = "You can save your shipping information for future use."; "lng_payments_card_title" = "New Card"; "lng_payments_card_number" = "Card Number"; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 5b7287066..aba6d5e5a 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -87,6 +87,7 @@ CheckoutProcess::CheckoutProcess( ) | rpl::start_with_next([=] { showForm(); }, _panel->lifetime()); + showForm(); } CheckoutProcess::~CheckoutProcess() { @@ -156,7 +157,7 @@ void CheckoutProcess::handleError(const Error &error) { showToast({ "Error: " + id }); } break; - case Error::Type::Validate: + case Error::Type::Validate: { if (_submitState == SubmitState::Validation) { _submitState = SubmitState::None; } @@ -189,7 +190,21 @@ void CheckoutProcess::handleError(const Error &error) { } else { showToast({ "Error: " + id }); } - break; + } break; + case Error::Type::Stripe: { + using Field = Ui::CardField; + if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) { + showCardError(Field::Number); + } else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) { + showCardError(Field::CVC); + } else if (id == u"InvalidExpiryMonth"_q + || id == u"InvalidExpiryYear"_q + || id == u"ExpiredCard"_q) { + showCardError(Field::ExpireDate); + } else { + showToast({ "Error: " + id }); + } + } break; case Error::Type::Send: if (_submitState == SubmitState::Finishing) { _submitState = SubmitState::None; @@ -375,6 +390,13 @@ void CheckoutProcess::showInformationError(Ui::InformationField field) { field); } +void CheckoutProcess::showCardError(Ui::CardField field) { + if (_submitState != SubmitState::None) { + return; + } + _panel->showCardError(_form->paymentMethod().ui.native, field); +} + void CheckoutProcess::chooseShippingOption() { _panel->chooseShippingOption(_form->shippingOptions()); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 636915071..de9d37724 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -19,6 +19,7 @@ class Session; namespace Payments::Ui { class Panel; enum class InformationField; +enum class CardField; } // namespace Payments::Ui namespace Payments { @@ -58,6 +59,7 @@ private: void showForm(); void showEditInformation(Ui::InformationField field); void showInformationError(Ui::InformationField field); + void showCardError(Ui::CardField field); void chooseShippingOption(); void editPaymentMethod(); diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 811f70160..20e44354a 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -56,3 +56,14 @@ paymentsIconName: icon {{ "payments/payment_name", menuIconFg }}; paymentsIconEmail: icon {{ "payments/payment_email", menuIconFg }}; paymentsIconPhone: icon {{ "payments/payment_phone", menuIconFg }}; paymentsIconShippingMethod: icon {{ "payments/payment_shipping", menuIconFg }}; + +paymentsField: defaultInputField; +paymentsFieldPadding: margins(28px, 0px, 28px, 2px); +paymentsExpireCvcSkip: 34px; + +paymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: windowActiveTextFg; + minWidth: 240px; +} +paymentsBillingInformationTitlePadding: margins(28px, 26px, 28px, 1px); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 7d5918e9f..8d647a174 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_edit_card.h" #include "payments/ui/payments_panel_delegate.h" -#include "passport/ui/passport_details_row.h" +#include "payments/ui/payments_field.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -52,16 +52,24 @@ EditCard::EditCard( void EditCard::setFocus(CardField field) { _focusField = field; - if (const auto control = controlForField(field)) { - _scroll->ensureWidgetVisible(control); + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); + control->setFocus(); + } +} + +void EditCard::setFocusFast(CardField field) { + _focusField = field; + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); control->setFocusFast(); } } void EditCard::showError(CardField field) { - if (const auto control = controlForField(field)) { - _scroll->ensureWidgetVisible(control); - control->showError(QString()); + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); + control->showError(); } } @@ -95,102 +103,69 @@ not_null EditCard::setupContent() { const auto showBox = [=](object_ptr box) { _delegate->panelShowBox(std::move(box)); }; - using Type = Passport::Ui::PanelDetailsType; - auto maxLabelWidth = 0; - accumulate_max( - maxLabelWidth, - Row::LabelWidth("Card Number")); - accumulate_max( - maxLabelWidth, - Row::LabelWidth("CVC")); - accumulate_max( - maxLabelWidth, - Row::LabelWidth("MM/YY")); + const auto add = [&](FieldConfig &&config) { + auto result = std::make_unique(inner, std::move(config)); + inner->add(result->ownedWidget(), st::paymentsFieldPadding); + return result; + }; + _number = add({ + .type = FieldType::CardNumber, + .placeholder = tr::lng_payments_card_number(), + .required = true, + }); if (_native.needCardholderName) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth("Cardholder Name")); + _name = add({ + .type = FieldType::CardNumber, + .placeholder = tr::lng_payments_card_holder(), + .required = true, + }); + } + auto container = inner->add( + object_ptr( + inner, + _number->widget()->height()), + st::paymentsFieldPadding); + _expire = std::make_unique(container, FieldConfig{ + .type = FieldType::CardExpireDate, + .placeholder = rpl::single(u"MM / YY"_q), + .required = true, + }); + _cvc = std::make_unique(container, FieldConfig{ + .type = FieldType::CardCVC, + .placeholder = rpl::single(u"CVC"_q), + .required = true, + }); + container->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto left = (width - st::paymentsExpireCvcSkip) / 2; + const auto right = width - st::paymentsExpireCvcSkip - left; + _expire->widget()->resizeToWidth(left); + _cvc->widget()->resizeToWidth(right); + _expire->widget()->moveToLeft(0, 0, width); + _cvc->widget()->moveToRight(0, 0, width); + }, container->lifetime()); + if (_native.needCountry || _native.needZip) { + inner->add( + object_ptr( + inner, + tr::lng_payments_billing_address(), + st::paymentsBillingInformationTitle), + st::paymentsBillingInformationTitlePadding); } if (_native.needCountry) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth("Billing Country")); + _country = add({ + .type = FieldType::Country, + .placeholder = tr::lng_payments_billing_country(), + .required = true, + }); } if (_native.needZip) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth("Billing Zip")); - } - _number = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - "Card Number", - maxLabelWidth, - QString(), - QString(), - 1024)); - _cvc = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - "CVC", - maxLabelWidth, - QString(), - QString(), - 1024)); - _expire = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - "MM/YY", - maxLabelWidth, - QString(), - QString(), - 1024)); - if (_native.needCardholderName) { - _name = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - "Cardholder Name", - maxLabelWidth, - QString(), - QString(), - 1024)); - } - if (_native.needCountry) { - _country = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Country, - "Billing Country", - maxLabelWidth, - QString(), - QString())); - } - if (_native.needZip) { - _zip = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Postcode, - "Billing Zip Code", - maxLabelWidth, - QString(), - QString(), - kMaxPostcodeSize)); + _zip = add({ + .type = FieldType::Text, + .placeholder = tr::lng_payments_billing_zip_code(), + .maxLength = kMaxPostcodeSize, + .required = true, + }); } return inner; } @@ -200,7 +175,7 @@ void EditCard::resizeEvent(QResizeEvent *e) { } void EditCard::focusInEvent(QFocusEvent *e) { - if (const auto control = controlForField(_focusField)) { + if (const auto control = lookupField(_focusField)) { control->setFocusFast(); } } @@ -218,27 +193,27 @@ void EditCard::updateControlsGeometry() { _scroll->updateBars(); } -auto EditCard::controlForField(CardField field) const -> Row* { +auto EditCard::lookupField(CardField field) const -> Field* { switch (field) { - case CardField::Number: return _number; - case CardField::CVC: return _cvc; - case CardField::ExpireDate: return _expire; - case CardField::Name: return _name; - case CardField::AddressCountry: return _country; - case CardField::AddressZip: return _zip; + case CardField::Number: return _number.get(); + case CardField::CVC: return _cvc.get(); + case CardField::ExpireDate: return _expire.get(); + case CardField::Name: return _name.get(); + case CardField::AddressCountry: return _country.get(); + case CardField::AddressZip: return _zip.get(); } Unexpected("Unknown field in EditCard::controlForField."); } UncheckedCardDetails EditCard::collect() const { return { - .number = _number ? _number->valueCurrent() : QString(), - .cvc = _cvc ? _cvc->valueCurrent() : QString(), - .expireYear = _expire ? ExtractYear(_expire->valueCurrent()) : 0, - .expireMonth = _expire ? ExtractMonth(_expire->valueCurrent()) : 0, - .cardholderName = _name ? _name->valueCurrent() : QString(), - .addressCountry = _country ? _country->valueCurrent() : QString(), - .addressZip = _zip ? _zip->valueCurrent() : QString(), + .number = _number ? _number->value() : QString(), + .cvc = _cvc ? _cvc->value() : QString(), + .expireYear = _expire ? ExtractYear(_expire->value()) : 0, + .expireMonth = _expire ? ExtractMonth(_expire->value()) : 0, + .cardholderName = _name ? _name->value() : QString(), + .addressCountry = _country ? _country->value() : QString(), + .addressZip = _zip ? _zip->value() : QString(), }; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h index 157e13edc..742bdbf34 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -17,15 +17,12 @@ class FadeShadow; class RoundButton; } // namespace Ui -namespace Passport::Ui { -class PanelDetailsRow; -} // namespace Passport::Ui - namespace Payments::Ui { using namespace ::Ui; class PanelDelegate; +class Field; class EditCard final : public RpWidget { public: @@ -35,19 +32,18 @@ public: CardField field, not_null delegate); - void showError(CardField field); void setFocus(CardField field); + void setFocusFast(CardField field); + void showError(CardField field); private: - using Row = Passport::Ui::PanelDetailsRow; - void resizeEvent(QResizeEvent *e) override; void focusInEvent(QFocusEvent *e) override; void setupControls(); [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); - [[nodiscard]] Row *controlForField(CardField field) const; + [[nodiscard]] Field *lookupField(CardField field) const; [[nodiscard]] UncheckedCardDetails collect() const; @@ -59,12 +55,12 @@ private: object_ptr _bottomShadow; object_ptr _done; - Row *_number = nullptr; - Row *_cvc = nullptr; - Row *_expire = nullptr; - Row *_name = nullptr; - Row *_country = nullptr; - Row *_zip = nullptr; + std::unique_ptr _number; + std::unique_ptr _cvc; + std::unique_ptr _expire; + std::unique_ptr _name; + std::unique_ptr _country; + std::unique_ptr _zip; CardField _focusField = CardField::Number; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index eff78b29b..8445c3815 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_edit_information.h" #include "payments/ui/payments_panel_delegate.h" -#include "passport/ui/passport_details_row.h" +#include "payments/ui/payments_field.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -48,18 +48,28 @@ EditInformation::EditInformation( setupControls(); } +EditInformation::~EditInformation() = default; + void EditInformation::setFocus(InformationField field) { _focusField = field; - if (const auto control = controlForField(field)) { - _scroll->ensureWidgetVisible(control); + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); + control->setFocus(); + } +} + +void EditInformation::setFocusFast(InformationField field) { + _focusField = field; + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); control->setFocusFast(); } } void EditInformation::showError(InformationField field) { - if (const auto control = controlForField(field)) { - _scroll->ensureWidgetVisible(control); - control->showError(QString()); + if (const auto control = lookupField(field)) { + _scroll->ensureWidgetVisible(control->widget()); + control->showError(); } } @@ -93,106 +103,44 @@ not_null EditInformation::setupContent() { const auto showBox = [=](object_ptr box) { _delegate->panelShowBox(std::move(box)); }; - using Type = Passport::Ui::PanelDetailsType; - auto maxLabelWidth = 0; + const auto add = [&](FieldConfig &&config) { + auto result = std::make_unique(inner, std::move(config)); + inner->add(result->ownedWidget(), st::paymentsFieldPadding); + return result; + }; if (_invoice.isShippingAddressRequested) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_passport_street(tr::now))); - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_passport_city(tr::now))); - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_passport_state(tr::now))); - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_passport_country(tr::now))); - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_passport_postcode(tr::now))); - } - if (_invoice.isNameRequested) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_payments_info_name(tr::now))); - } - if (_invoice.isEmailRequested) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_payments_info_email(tr::now))); - } - if (_invoice.isPhoneRequested) { - accumulate_max( - maxLabelWidth, - Row::LabelWidth(tr::lng_payments_info_phone(tr::now))); - } - if (_invoice.isShippingAddressRequested) { - _street1 = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_passport_street(tr::now), - maxLabelWidth, - _information.shippingAddress.address1, - QString(), - kMaxStreetSize)); - _street2 = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_passport_street(tr::now), - maxLabelWidth, - _information.shippingAddress.address2, - QString(), - kMaxStreetSize)); - _city = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_passport_city(tr::now), - maxLabelWidth, - _information.shippingAddress.city, - QString(), - kMaxStreetSize)); - _state = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_passport_state(tr::now), - maxLabelWidth, - _information.shippingAddress.state, - QString(), - kMaxStreetSize)); - _country = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Country, - tr::lng_passport_country(tr::now), - maxLabelWidth, - _information.shippingAddress.countryIso2, - QString())); - _postcode = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Postcode, - tr::lng_passport_postcode(tr::now), - maxLabelWidth, - _information.shippingAddress.postcode, - QString(), - kMaxPostcodeSize)); + _street1 = add({ + .placeholder = tr::lng_payments_address_street1(), + .value = _information.shippingAddress.address1, + .maxLength = kMaxStreetSize, + .required = true, + }); + _street2 = add({ + .placeholder = tr::lng_payments_address_street2(), + .value = _information.shippingAddress.address2, + .maxLength = kMaxStreetSize, + }); + _city = add({ + .placeholder = tr::lng_payments_address_city(), + .value = _information.shippingAddress.city, + .required = true, + }); + _state = add({ + .placeholder = tr::lng_payments_address_state(), + .value = _information.shippingAddress.state, + }); + _country = add({ + .type = FieldType::Country, + .placeholder = tr::lng_payments_address_country(), + .value = _information.shippingAddress.countryIso2, + .required = true, + }); + _postcode = add({ + .placeholder = tr::lng_payments_address_postcode(), + .value = _information.shippingAddress.postcode, + .maxLength = kMaxPostcodeSize, + .required = true, + }); //StreetValidate, // #TODO payments //CityValidate, //CountryValidate, @@ -200,43 +148,28 @@ not_null EditInformation::setupContent() { //PostcodeValidate, } if (_invoice.isNameRequested) { - _name = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_payments_info_name(tr::now), - maxLabelWidth, - _information.name, - QString(), - kMaxNameSize)); + _name = add({ + .placeholder = tr::lng_payments_info_name(), + .value = _information.name, + .maxLength = kMaxNameSize, + .required = true, + }); } if (_invoice.isEmailRequested) { - _email = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_payments_info_email(tr::now), - maxLabelWidth, - _information.email, - QString(), - kMaxEmailSize)); + _email = add({ + .placeholder = tr::lng_payments_info_email(), + .value = _information.email, + .maxLength = kMaxEmailSize, + .required = true, + }); } if (_invoice.isPhoneRequested) { - _phone = inner->add( - Row::Create( - inner, - showBox, - QString(), - Type::Text, - tr::lng_payments_info_phone(tr::now), - maxLabelWidth, - _information.phone, - QString(), - kMaxPhoneSize)); + _phone = add({ + .placeholder = tr::lng_payments_info_phone(), + .value = _information.phone, + .maxLength = kMaxPhoneSize, + .required = true, + }); } return inner; } @@ -246,8 +179,8 @@ void EditInformation::resizeEvent(QResizeEvent *e) { } void EditInformation::focusInEvent(QFocusEvent *e) { - if (const auto control = controlForField(_focusField)) { - control->setFocusFast(); + if (const auto control = lookupField(_focusField)) { + control->setFocus(); } } @@ -264,32 +197,32 @@ void EditInformation::updateControlsGeometry() { _scroll->updateBars(); } -auto EditInformation::controlForField(InformationField field) const -> Row* { +auto EditInformation::lookupField(InformationField field) const -> Field* { switch (field) { - case InformationField::ShippingStreet: return _street1; - case InformationField::ShippingCity: return _city; - case InformationField::ShippingState: return _state; - case InformationField::ShippingCountry: return _country; - case InformationField::ShippingPostcode: return _postcode; - case InformationField::Name: return _name; - case InformationField::Email: return _email; - case InformationField::Phone: return _phone; + case InformationField::ShippingStreet: return _street1.get(); + case InformationField::ShippingCity: return _city.get(); + case InformationField::ShippingState: return _state.get(); + case InformationField::ShippingCountry: return _country.get(); + case InformationField::ShippingPostcode: return _postcode.get(); + case InformationField::Name: return _name.get(); + case InformationField::Email: return _email.get(); + case InformationField::Phone: return _phone.get(); } - Unexpected("Unknown field in EditInformation::controlForField."); + Unexpected("Unknown field in EditInformation::lookupField."); } RequestedInformation EditInformation::collect() const { return { - .name = _name ? _name->valueCurrent() : QString(), - .phone = _phone ? _phone->valueCurrent() : QString(), - .email = _email ? _email->valueCurrent() : QString(), + .name = _name ? _name->value() : QString(), + .phone = _phone ? _phone->value() : QString(), + .email = _email ? _email->value() : QString(), .shippingAddress = { - .address1 = _street1 ? _street1->valueCurrent() : QString(), - .address2 = _street2 ? _street2->valueCurrent() : QString(), - .city = _city ? _city->valueCurrent() : QString(), - .state = _state ? _state->valueCurrent() : QString(), - .countryIso2 = _country ? _country->valueCurrent() : QString(), - .postcode = _postcode ? _postcode->valueCurrent() : QString(), + .address1 = _street1 ? _street1->value() : QString(), + .address2 = _street2 ? _street2->value() : QString(), + .city = _city ? _city->value() : QString(), + .state = _state ? _state->value() : QString(), + .countryIso2 = _country ? _country->value() : QString(), + .postcode = _postcode ? _postcode->value() : QString(), }, }; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h index d4f8ee78e..978b72b43 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -15,17 +15,16 @@ namespace Ui { class ScrollArea; class FadeShadow; class RoundButton; +class InputField; +class MaskedInputField; } // namespace Ui -namespace Passport::Ui { -class PanelDetailsRow; -} // namespace Passport::Ui - namespace Payments::Ui { using namespace ::Ui; class PanelDelegate; +class Field; class EditInformation final : public RpWidget { public: @@ -35,20 +34,20 @@ public: const RequestedInformation ¤t, InformationField field, not_null delegate); + ~EditInformation(); - void showError(InformationField field); void setFocus(InformationField field); + void setFocusFast(InformationField field); + void showError(InformationField field); private: - using Row = Passport::Ui::PanelDetailsRow; - void resizeEvent(QResizeEvent *e) override; void focusInEvent(QFocusEvent *e) override; void setupControls(); [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); - [[nodiscard]] Row *controlForField(InformationField field) const; + [[nodiscard]] Field *lookupField(InformationField field) const; [[nodiscard]] RequestedInformation collect() const; @@ -61,15 +60,15 @@ private: object_ptr _bottomShadow; object_ptr _done; - Row *_street1 = nullptr; - Row *_street2 = nullptr; - Row *_city = nullptr; - Row *_state = nullptr; - Row *_country = nullptr; - Row *_postcode = nullptr; - Row *_name = nullptr; - Row *_email = nullptr; - Row *_phone = nullptr; + std::unique_ptr _street1; + std::unique_ptr _street2; + std::unique_ptr _city; + std::unique_ptr _state; + std::unique_ptr _country; + std::unique_ptr _postcode; + std::unique_ptr _name; + std::unique_ptr _email; + std::unique_ptr _phone; InformationField _focusField = InformationField::ShippingStreet; diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp new file mode 100644 index 000000000..2161772f9 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -0,0 +1,140 @@ +/* +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_field.h" + +#include "ui/widgets/input_fields.h" +#include "styles/style_payments.h" + +namespace Payments::Ui { +namespace { + +[[nodiscard]] bool UseMaskedField(FieldType type) { + switch (type) { + case FieldType::Text: + case FieldType::Email: + return false; + case FieldType::CardNumber: + case FieldType::CardExpireDate: + case FieldType::CardCVC: + case FieldType::Country: + case FieldType::Phone: + return true; + } + Unexpected("FieldType in Payments::Ui::UseMaskedField."); +} + +[[nodiscard]] base::unique_qptr CreateWrap( + QWidget *parent, + FieldConfig &config) { + switch (config.type) { + case FieldType::Text: + case FieldType::Email: + return base::make_unique_q( + parent, + st::paymentsField, + std::move(config.placeholder), + config.value); + case FieldType::CardNumber: + case FieldType::CardExpireDate: + case FieldType::CardCVC: + case FieldType::Country: + case FieldType::Phone: + return base::make_unique_q(parent); + } + Unexpected("FieldType in Payments::Ui::CreateWrap."); +} + +[[nodiscard]] InputField *LookupInputField( + not_null wrap, + FieldConfig &config) { + return UseMaskedField(config.type) + ? nullptr + : static_cast(wrap.get()); +} + +[[nodiscard]] MaskedInputField *LookupMaskedField( + not_null wrap, + FieldConfig &config) { + if (!UseMaskedField(config.type)) { + return nullptr; + } + switch (config.type) { + case FieldType::Text: + case FieldType::Email: + return nullptr; + case FieldType::CardNumber: + case FieldType::CardExpireDate: + case FieldType::CardCVC: + case FieldType::Country: + case FieldType::Phone: + return CreateChild( + wrap.get(), + st::paymentsField, + std::move(config.placeholder), + config.value); + } + Unexpected("FieldType in Payments::Ui::LookupMaskedField."); +} + +} // namespace + +Field::Field(QWidget *parent, FieldConfig &&config) +: _type(config.type) +, _wrap(CreateWrap(parent, config)) +, _input(LookupInputField(_wrap.get(), config)) +, _masked(LookupMaskedField(_wrap.get(), config)) { + if (_masked) { + _wrap->resize(_masked->size()); + _wrap->widthValue( + ) | rpl::start_with_next([=](int width) { + _masked->resize(width, _masked->height()); + }, _masked->lifetime()); + _masked->heightValue( + ) | rpl::start_with_next([=](int height) { + _wrap->resize(_wrap->width(), height); + }, _masked->lifetime()); + } +} + +RpWidget *Field::widget() const { + return _wrap.get(); +} + +object_ptr Field::ownedWidget() const { + return object_ptr::fromRaw(_wrap.get()); +} + +[[nodiscard]] QString Field::value() const { + return _input ? _input->getLastText() : _masked->getLastText(); +} + +void Field::setFocus() { + if (_input) { + _input->setFocus(); + } else { + _masked->setFocus(); + } +} + +void Field::setFocusFast() { + if (_input) { + _input->setFocusFast(); + } else { + _masked->setFocusFast(); + } +} + +void Field::showError() { + if (_input) { + _input->showError(); + } else { + _masked->showError(); + } +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h new file mode 100644 index 000000000..92277ac36 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -0,0 +1,62 @@ +/* +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 "base/object_ptr.h" +#include "base/unique_qptr.h" + +namespace Ui { +class RpWidget; +class InputField; +class MaskedInputField; +} // namespace Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +enum class FieldType { + Text, + CardNumber, + CardExpireDate, + CardCVC, + Country, + Phone, + Email, +}; + +struct FieldConfig { + FieldType type = FieldType::Text; + rpl::producer placeholder; + QString value; + int maxLength = 0; + bool required = false; +}; + +class Field final { +public: + Field(QWidget *parent, FieldConfig &&config); + + [[nodiscard]] RpWidget *widget() const; + [[nodiscard]] object_ptr ownedWidget() const; + + [[nodiscard]] QString value() const; + + void setFocus(); + void setFocusFast(); + void showError(); + +private: + const FieldType _type = FieldType::Text; + const base::unique_qptr _wrap; + InputField *_input = nullptr; + MaskedInputField *_masked = nullptr; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 0f63805be..68c5965c6 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -58,7 +58,7 @@ QString FormSummary::formatAmount(int64 amount) const { const auto base = FillAmountAndCurrency( std::abs(amount), _invoice.currency); - return (amount > 0) ? base : (QString::fromUtf8("\xe2\x88\x92") + base); + return (amount < 0) ? (QString::fromUtf8("\xe2\x88\x92") + base) : base; } int64 FormSummary::computeTotalAmount() const { @@ -87,6 +87,9 @@ void FormSummary::setupControls() { _submit->addClickHandler([=] { _delegate->panelSubmit(); }); + if (!_invoice) { + _submit->hide(); + } using namespace rpl::mappers; @@ -317,10 +320,12 @@ not_null FormSummary::setupContent() { }, inner->lifetime()); setupCover(inner); - Settings::AddDivider(inner); - setupPrices(inner); - Settings::AddDivider(inner); - setupSections(inner); + if (_invoice) { + Settings::AddDivider(inner); + setupPrices(inner); + Settings::AddDivider(inner); + setupSections(inner); + } return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 1ae7054ec..d39a7ab69 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -23,7 +23,6 @@ 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->setWindowFlag(Qt::WindowStaysOnTopHint, false); @@ -52,6 +51,7 @@ void Panel::showForm( const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options) { + _widget->setTitle(tr::lng_payments_checkout_title()); auto form = base::make_unique_q( _widget.get(), invoice, @@ -74,6 +74,7 @@ void Panel::showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, InformationField field) { + _widget->setTitle(tr::lng_payments_shipping_address_title()); auto edit = base::make_unique_q( _widget.get(), invoice, @@ -83,7 +84,7 @@ void Panel::showEditInformation( _weakEditInformation = edit.get(); _widget->showInner(std::move(edit)); _widget->setBackAllowed(true); - _weakEditInformation->setFocus(field); + _weakEditInformation->setFocusFast(field); } void Panel::showInformationError( @@ -125,6 +126,7 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { } void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { + _widget->setTitle(tr::lng_payments_card_title()); if (method.native.supported) { showEditCard(method.native, CardField::Number); } else if (!showWebview(method.url, true)) { @@ -221,7 +223,7 @@ void Panel::showEditCard( _weakEditCard = edit.get(); _widget->showInner(std::move(edit)); _widget->setBackAllowed(true); - _weakEditCard->setFocus(field); + _weakEditCard->setFocusFast(field); } void Panel::showCardError( @@ -230,11 +232,12 @@ void Panel::showCardError( if (_weakEditCard) { _weakEditCard->showError(field); } else { - showEditCard(native, field); - if (_weakEditCard - && field == CardField::AddressCountry) { - _weakEditCard->showError(field); - } + // We cancelled card edit already. + //showEditCard(native, field); + //if (_weakEditCard + // && field == CardField::AddressCountry) { + // _weakEditCard->showError(field); + //} } } diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index fd8ada4b0..66b94f63f 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -75,6 +75,8 @@ PRIVATE payments/ui/payments_edit_information.h payments/ui/payments_form_summary.cpp payments/ui/payments_form_summary.h + payments/ui/payments_field.cpp + payments/ui/payments_field.h payments/ui/payments_panel.cpp payments/ui/payments_panel.h payments/ui/payments_panel_data.h From e0771633223124f2d679440b594a2695f92b14c6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Mar 2021 21:09:09 +0400 Subject: [PATCH 021/127] Add nice country choosing in payments. --- .../SourceFiles/payments/payments_form.cpp | 35 ++++++ Telegram/SourceFiles/payments/payments_form.h | 14 ++- .../payments/ui/payments_edit_card.cpp | 2 + .../payments/ui/payments_edit_information.cpp | 2 + .../payments/ui/payments_field.cpp | 105 +++++++++++++++--- .../SourceFiles/payments/ui/payments_field.h | 9 +- .../payments/ui/payments_panel_data.h | 4 + 7 files changed, 151 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 6114bcb65..702d4d5f1 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_file_origin.h" +#include "data/data_countries.h" #include "history/history_item.h" #include "stripe/stripe_api_client.h" #include "stripe/stripe_error.h" @@ -269,6 +270,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); _savedInformation = Ui::RequestedInformation{ + .defaultCountry = defaultCountry(), .name = qs(data.vname().value_or_empty()), .phone = qs(data.vphone().value_or_empty()), .email = qs(data.vemail().value_or_empty()), @@ -291,6 +293,11 @@ void Form::refreshPaymentMethodDetails() { const auto &entered = _paymentMethod.newCredentials; _paymentMethod.ui.title = entered ? entered.title : saved.title; _paymentMethod.ui.ready = entered || saved; + _paymentMethod.ui.native.defaultCountry = defaultCountry(); +} + +QString Form::defaultCountry() const { + return Data::CountryISO2ByPhone(_session->user()->phone()); } void Form::fillPaymentMethodInformation() { @@ -375,6 +382,10 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _api.request(base::take(_validateRequestId)).cancel(); } _validatedInformation = information; + if (const auto error = localInformationError(information)) { + _updates.fire_copy(error); + return; + } _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( MTP_flags(0), // #TODO payments save information MTP_int(_msgId.msg), @@ -404,6 +415,30 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { }).send(); } +Error Form::localInformationError( + const Ui::RequestedInformation &information) const { + const auto error = [](const QString &id) { + return Error{ Error::Type::Validate, id }; + }; + if (_invoice.isShippingAddressRequested + && !information.shippingAddress) { + return information.shippingAddress.address1.isEmpty() + ? error(u"ADDRESS_STREET_LINE1_INVALID"_q) + : information.shippingAddress.city.isEmpty() + ? error(u"ADDRESS_CITY_INVALID"_q) + : information.shippingAddress.countryIso2.isEmpty() + ? error(u"ADDRESS_COUNTRY_INVALID"_q) + : (Unexpected("Shipping Address error."), Error()); + } else if (_invoice.isNameRequested && information.name.isEmpty()) { + return error(u"REQ_INFO_NAME_INVALID"_q); + } else if (_invoice.isEmailRequested && information.email.isEmpty()) { + return error(u"REQ_INFO_EMAIL_INVALID"_q); + } else if (_invoice.isPhoneRequested && information.phone.isEmpty()) { + return error(u"REQ_INFO_PHONE_INVALID"_q); + } + return Error(); +} + void Form::validateCard(const Ui::UncheckedCardDetails &details) { Expects(!v::is_null(_paymentMethod.native.data)); diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 803dd4b3f..367512e2b 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -113,13 +113,21 @@ struct PaymentFinished { }; struct Error { enum class Type { + None, Form, Validate, Stripe, Send, }; - Type type = Type::Form; + Type type = Type::None; QString id; + + [[nodiscard]] bool empty() const { + return (type == Type::None); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } }; struct FormUpdate : std::variant< @@ -188,11 +196,15 @@ private: void fillPaymentMethodInformation(); void fillStripeNativeMethod(); void refreshPaymentMethodDetails(); + [[nodiscard]] QString defaultCountry() const; void validateCard( const StripePaymentMethod &method, const Ui::UncheckedCardDetails &details); + [[nodiscard]] Error localInformationError( + const Ui::RequestedInformation &information) const; + const not_null _session; MTP::Sender _api; FullMsgId _msgId; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 8d647a174..0d25d4995 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -156,6 +156,8 @@ not_null EditCard::setupContent() { _country = add({ .type = FieldType::Country, .placeholder = tr::lng_payments_billing_country(), + .showBox = showBox, + .defaultCountry = _native.defaultCountry, .required = true, }); } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 8445c3815..522878202 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -133,6 +133,8 @@ not_null EditInformation::setupContent() { .type = FieldType::Country, .placeholder = tr::lng_payments_address_country(), .value = _information.shippingAddress.countryIso2, + .showBox = showBox, + .defaultCountry = _information.defaultCountry, .required = true, }); _postcode = add({ diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 2161772f9..0fc86222a 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -8,11 +8,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_field.h" #include "ui/widgets/input_fields.h" +#include "ui/boxes/country_select_box.h" +#include "data/data_countries.h" +#include "base/platform/base_platform_info.h" #include "styles/style_payments.h" namespace Payments::Ui { namespace { +[[nodiscard]] QString Parse(const FieldConfig &config) { + if (config.type == FieldType::Country) { + return Data::CountryNameByISO2(config.value); + } + return config.value; +} + +[[nodiscard]] QString Format( + const FieldConfig &config, + const QString &parsed, + const QString &countryIso2) { + if (config.type == FieldType::Country) { + return countryIso2; + } + return parsed; +} + [[nodiscard]] bool UseMaskedField(FieldType type) { switch (type) { case FieldType::Text: @@ -38,7 +58,7 @@ namespace { parent, st::paymentsField, std::move(config.placeholder), - config.value); + Parse(config)); case FieldType::CardNumber: case FieldType::CardExpireDate: case FieldType::CardCVC: @@ -76,7 +96,7 @@ namespace { wrap.get(), st::paymentsField, std::move(config.placeholder), - config.value); + Parse(config)); } Unexpected("FieldType in Payments::Ui::LookupMaskedField."); } @@ -84,20 +104,16 @@ namespace { } // namespace Field::Field(QWidget *parent, FieldConfig &&config) -: _type(config.type) +: _config(config) , _wrap(CreateWrap(parent, config)) , _input(LookupInputField(_wrap.get(), config)) -, _masked(LookupMaskedField(_wrap.get(), config)) { +, _masked(LookupMaskedField(_wrap.get(), config)) +, _countryIso2(config.value) { if (_masked) { - _wrap->resize(_masked->size()); - _wrap->widthValue( - ) | rpl::start_with_next([=](int width) { - _masked->resize(width, _masked->height()); - }, _masked->lifetime()); - _masked->heightValue( - ) | rpl::start_with_next([=](int height) { - _wrap->resize(_wrap->width(), height); - }, _masked->lifetime()); + setupMaskedGeometry(); + } + if (_config.type == FieldType::Country) { + setupCountry(); } } @@ -109,12 +125,60 @@ object_ptr Field::ownedWidget() const { return object_ptr::fromRaw(_wrap.get()); } -[[nodiscard]] QString Field::value() const { - return _input ? _input->getLastText() : _masked->getLastText(); +QString Field::value() const { + return Format( + _config, + _input ? _input->getLastText() : _masked->getLastText(), + _countryIso2); +} + +void Field::setupMaskedGeometry() { + Expects(_masked != nullptr); + + _wrap->resize(_masked->size()); + _wrap->widthValue( + ) | rpl::start_with_next([=](int width) { + _masked->resize(width, _masked->height()); + }, _masked->lifetime()); + _masked->heightValue( + ) | rpl::start_with_next([=](int height) { + _wrap->resize(_wrap->width(), height); + }, _masked->lifetime()); +} + +void Field::setupCountry() { + Expects(_config.type == FieldType::Country); + Expects(_masked != nullptr); + + QObject::connect(_masked, &MaskedInputField::focused, [=] { + setFocus(); + + const auto name = Data::CountryNameByISO2(_countryIso2); + const auto country = !name.isEmpty() + ? _countryIso2 + : !_config.defaultCountry.isEmpty() + ? _config.defaultCountry + : Platform::SystemCountry(); + auto box = Box( + country, + CountrySelectBox::Type::Countries); + const auto raw = box.data(); + raw->countryChosen( + ) | rpl::start_with_next([=](QString iso2) { + _countryIso2 = iso2; + _masked->setText(Data::CountryNameByISO2(iso2)); + _masked->hideError(); + setFocus(); + raw->closeBox(); + }, _masked->lifetime()); + _config.showBox(std::move(box)); + }); } void Field::setFocus() { - if (_input) { + if (_config.type == FieldType::Country) { + _wrap->setFocus(); + } else if (_input) { _input->setFocus(); } else { _masked->setFocus(); @@ -122,7 +186,9 @@ void Field::setFocus() { } void Field::setFocusFast() { - if (_input) { + if (_config.type == FieldType::Country) { + setFocus(); + } else if (_input) { _input->setFocusFast(); } else { _masked->setFocusFast(); @@ -130,7 +196,10 @@ void Field::setFocusFast() { } void Field::showError() { - if (_input) { + if (_config.type == FieldType::Country) { + setFocus(); + _masked->showErrorNoFocus(); + } else if (_input) { _input->showError(); } else { _masked->showError(); diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index 92277ac36..c82b4d2fe 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -14,6 +14,7 @@ namespace Ui { class RpWidget; class InputField; class MaskedInputField; +class BoxContent; } // namespace Ui namespace Payments::Ui { @@ -34,6 +35,8 @@ struct FieldConfig { FieldType type = FieldType::Text; rpl::producer placeholder; QString value; + Fn)> showBox; + QString defaultCountry; int maxLength = 0; bool required = false; }; @@ -52,10 +55,14 @@ public: void showError(); private: - const FieldType _type = FieldType::Text; + void setupMaskedGeometry(); + void setupCountry(); + + const FieldConfig _config; const base::unique_qptr _wrap; InputField *_input = nullptr; MaskedInputField *_masked = nullptr; + QString _countryIso2; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 03f37aa4e..cf97ac5c7 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -87,6 +87,8 @@ struct Address { }; struct RequestedInformation { + QString defaultCountry; + QString name; QString phone; QString email; @@ -125,6 +127,8 @@ enum class InformationField { }; struct NativeMethodDetails { + QString defaultCountry; + bool supported = false; bool needCountry = false; bool needZip = false; From 0af6c4b0b6981a2be610ff462d07636ff2635613 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 Mar 2021 16:16:54 +0400 Subject: [PATCH 022/127] Add local validation for card information. --- Telegram/CMakeLists.txt | 2 - .../SourceFiles/boxes/change_phone_box.cpp | 2 +- .../SourceFiles/boxes/confirm_phone_box.cpp | 8 - .../SourceFiles/boxes/confirm_phone_box.h | 1 - Telegram/SourceFiles/config.h | 3 - .../passport/passport_panel_edit_contact.cpp | 3 +- .../payments/payments_checkout_process.cpp | 39 ++- .../SourceFiles/payments/payments_form.cpp | 121 ++++++-- Telegram/SourceFiles/payments/payments_form.h | 11 +- .../SourceFiles/payments/stripe/stripe_card.h | 1 + .../payments/stripe/stripe_card_validator.cpp | 279 ++++++++++++++++++ .../payments/stripe/stripe_card_validator.h | 51 ++++ .../payments/ui/payments_edit_card.cpp | 211 ++++++++++++- .../payments/ui/payments_edit_information.cpp | 33 +-- .../payments/ui/payments_field.cpp | 156 +++++++++- .../SourceFiles/payments/ui/payments_field.h | 69 ++++- .../payments/ui/payments_form_summary.cpp | 4 +- .../payments/ui/payments_panel_data.h | 3 +- .../ui/boxes/country_select_box.cpp | 16 +- Telegram/SourceFiles/ui/special_fields.cpp | 26 +- Telegram/SourceFiles/ui/special_fields.h | 2 + Telegram/cmake/lib_stripe.cmake | 2 + Telegram/cmake/td_ui.cmake | 5 +- 23 files changed, 950 insertions(+), 98 deletions(-) create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp create mode 100644 Telegram/SourceFiles/payments/stripe/stripe_card_validator.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 9f5defbd5..a183df1c4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1029,8 +1029,6 @@ PRIVATE ui/search_field_controller.h ui/special_buttons.cpp ui/special_buttons.h - ui/special_fields.cpp - ui/special_fields.h ui/unread_badge.cpp ui/unread_badge.h window/main_window.cpp diff --git a/Telegram/SourceFiles/boxes/change_phone_box.cpp b/Telegram/SourceFiles/boxes/change_phone_box.cpp index 727c8b93c..199ebeab5 100644 --- a/Telegram/SourceFiles/boxes/change_phone_box.cpp +++ b/Telegram/SourceFiles/boxes/change_phone_box.cpp @@ -151,7 +151,7 @@ void ChangePhoneBox::EnterPhone::prepare() { this, st::defaultInputField, tr::lng_change_phone_new_title(), - ExtractPhonePrefix(_session->user()->phone()), + Ui::ExtractPhonePrefix(_session->user()->phone()), phoneValue); _phone->resize(st::boxWidth - 2 * st::boxPadding.left(), _phone->height()); diff --git a/Telegram/SourceFiles/boxes/confirm_phone_box.cpp b/Telegram/SourceFiles/boxes/confirm_phone_box.cpp index e0d4bf803..3d960f668 100644 --- a/Telegram/SourceFiles/boxes/confirm_phone_box.cpp +++ b/Telegram/SourceFiles/boxes/confirm_phone_box.cpp @@ -71,14 +71,6 @@ void ShowPhoneBannedError(const QString &phone) { [=] { SendToBannedHelp(phone); close(); })); } -QString ExtractPhonePrefix(const QString &phone) { - const auto pattern = phoneNumberParse(phone); - if (!pattern.isEmpty()) { - return phone.mid(0, pattern[0]); - } - return QString(); -} - SentCodeField::SentCodeField( QWidget *parent, const style::InputField &st, diff --git a/Telegram/SourceFiles/boxes/confirm_phone_box.h b/Telegram/SourceFiles/boxes/confirm_phone_box.h index cb3b15de8..5448a78f8 100644 --- a/Telegram/SourceFiles/boxes/confirm_phone_box.h +++ b/Telegram/SourceFiles/boxes/confirm_phone_box.h @@ -22,7 +22,6 @@ class Session; } // namespace Main void ShowPhoneBannedError(const QString &phone); -[[nodiscard]] QString ExtractPhonePrefix(const QString &phone); class SentCodeField : public Ui::InputField { public: diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 57a8ac904..978089c50 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -13,9 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL enum { MaxSelectedItems = 100, - MaxPhoneCodeLength = 4, // max length of country phone code - MaxPhoneTailLength = 32, // rest of the phone number, without country code (seen 12 at least), need more for service numbers - LocalEncryptIterCount = 4000, // key derivation iteration count LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway) LocalEncryptSaltSize = 32, // 256 bit diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp index 3b9986ade..59ca85cd9 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp @@ -278,7 +278,8 @@ void PanelEditContact::setupControls( wrap.data(), fieldStyle, std::move(fieldPlaceholder), - ExtractPhonePrefix(_controller->bot()->session().user()->phone()), + Ui::ExtractPhonePrefix( + _controller->bot()->session().user()->phone()), data); } else { _field = Ui::CreateChild( diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index aba6d5e5a..7d67fb94e 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -166,23 +166,36 @@ void CheckoutProcess::handleError(const Error &error) { showForm(); return; } - using Field = Ui::InformationField; + using InfoField = Ui::InformationField; + using CardField = Ui::CardField; if (id == u"REQ_INFO_NAME_INVALID"_q) { - showInformationError(Field::Name); + showInformationError(InfoField::Name); } else if (id == u"REQ_INFO_EMAIL_INVALID"_q) { - showInformationError(Field::Email); + showInformationError(InfoField::Email); } else if (id == u"REQ_INFO_PHONE_INVALID"_q) { - showInformationError(Field::Phone); + showInformationError(InfoField::Phone); } else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) { - showInformationError(Field::ShippingStreet); + showInformationError(InfoField::ShippingStreet); } else if (id == u"ADDRESS_CITY_INVALID"_q) { - showInformationError(Field::ShippingCity); + showInformationError(InfoField::ShippingCity); } else if (id == u"ADDRESS_STATE_INVALID"_q) { - showInformationError(Field::ShippingState); + showInformationError(InfoField::ShippingState); } else if (id == u"ADDRESS_COUNTRY_INVALID"_q) { - showInformationError(Field::ShippingCountry); + showInformationError(InfoField::ShippingCountry); } else if (id == u"ADDRESS_POSTCODE_INVALID"_q) { - showInformationError(Field::ShippingPostcode); + showInformationError(InfoField::ShippingPostcode); + } else if (id == u"LOCAL_CARD_NUMBER_INVALID"_q) { + showCardError(CardField::Number); + } else if (id == u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q) { + showCardError(CardField::ExpireDate); + } else if (id == u"LOCAL_CARD_CVC_INVALID"_q) { + showCardError(CardField::Cvc); + } else if (id == u"LOCAL_CARD_HOLDER_NAME_INVALID"_q) { + showCardError(CardField::Name); + } else if (id == u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q) { + showCardError(CardField::AddressCountry); + } else if (id == u"LOCAL_CARD_BILLING_ZIP_INVALID"_q) { + showCardError(CardField::AddressZip); } else if (id == u"SHIPPING_BOT_TIMEOUT"_q) { showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message } else if (id == u"SHIPPING_NOT_AVAILABLE"_q) { @@ -196,11 +209,17 @@ void CheckoutProcess::handleError(const Error &error) { if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) { showCardError(Field::Number); } else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) { - showCardError(Field::CVC); + showCardError(Field::Cvc); } else if (id == u"InvalidExpiryMonth"_q || id == u"InvalidExpiryYear"_q || id == u"ExpiredCard"_q) { showCardError(Field::ExpireDate); + } else if (id == u"CardDeclined"_q) { + // #TODO payments errors message + showToast({ "Error: " + id }); + } else if (id == u"ProcessingError"_q) { + // #TODO payments errors message + showToast({ "Error: " + id }); } else { showToast({ "Error: " + id }); } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 702d4d5f1..96dfa27c1 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "stripe/stripe_api_client.h" #include "stripe/stripe_error.h" #include "stripe/stripe_token.h" +#include "stripe/stripe_card_validator.h" #include "ui/image/image.h" #include "apiwrap.h" #include "styles/style_payments.h" // paymentsThumbnailSize. @@ -270,6 +271,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); _savedInformation = Ui::RequestedInformation{ + .defaultPhone = defaultPhone(), .defaultCountry = defaultCountry(), .name = qs(data.vname().value_or_empty()), .phone = qs(data.vphone().value_or_empty()), @@ -293,11 +295,16 @@ void Form::refreshPaymentMethodDetails() { const auto &entered = _paymentMethod.newCredentials; _paymentMethod.ui.title = entered ? entered.title : saved.title; _paymentMethod.ui.ready = entered || saved; + _paymentMethod.ui.native.defaultPhone = defaultPhone(); _paymentMethod.ui.native.defaultCountry = defaultCountry(); } +QString Form::defaultPhone() const { + return _session->user()->phone(); +} + QString Form::defaultCountry() const { - return Data::CountryISO2ByPhone(_session->user()->phone()); + return Data::CountryISO2ByPhone(defaultPhone()); } void Form::fillPaymentMethodInformation() { @@ -382,10 +389,16 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _api.request(base::take(_validateRequestId)).cancel(); } _validatedInformation = information; - if (const auto error = localInformationError(information)) { - _updates.fire_copy(error); + if (!validateInformationLocal(information)) { return; } + + Assert(!_invoice.isShippingAddressRequested + || information.shippingAddress); + Assert(!_invoice.isNameRequested || !information.name.isEmpty()); + Assert(!_invoice.isEmailRequested || !information.email.isEmpty()); + Assert(!_invoice.isPhoneRequested || !information.phone.isEmpty()); + _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( MTP_flags(0), // #TODO payments save information MTP_int(_msgId.msg), @@ -415,26 +428,43 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { }).send(); } -Error Form::localInformationError( +bool Form::validateInformationLocal( const Ui::RequestedInformation &information) const { - const auto error = [](const QString &id) { - return Error{ Error::Type::Validate, id }; + if (const auto error = informationErrorLocal(information)) { + _updates.fire_copy(error); + return false; + } + return true; +} + +Error Form::informationErrorLocal( + const Ui::RequestedInformation &information) const { + auto errors = QStringList(); + const auto push = [&](const QString &id) { + errors.push_back(id); }; - if (_invoice.isShippingAddressRequested - && !information.shippingAddress) { - return information.shippingAddress.address1.isEmpty() - ? error(u"ADDRESS_STREET_LINE1_INVALID"_q) - : information.shippingAddress.city.isEmpty() - ? error(u"ADDRESS_CITY_INVALID"_q) - : information.shippingAddress.countryIso2.isEmpty() - ? error(u"ADDRESS_COUNTRY_INVALID"_q) - : (Unexpected("Shipping Address error."), Error()); - } else if (_invoice.isNameRequested && information.name.isEmpty()) { - return error(u"REQ_INFO_NAME_INVALID"_q); - } else if (_invoice.isEmailRequested && information.email.isEmpty()) { - return error(u"REQ_INFO_EMAIL_INVALID"_q); - } else if (_invoice.isPhoneRequested && information.phone.isEmpty()) { - return error(u"REQ_INFO_PHONE_INVALID"_q); + if (_invoice.isShippingAddressRequested) { + if (information.shippingAddress.address1.isEmpty()) { + push(u"ADDRESS_STREET_LINE1_INVALID"_q); + } + if (information.shippingAddress.city.isEmpty()) { + push(u"ADDRESS_CITY_INVALID"_q); + } + if (information.shippingAddress.countryIso2.isEmpty()) { + push(u"ADDRESS_COUNTRY_INVALID"_q); + } + } + if (_invoice.isNameRequested && information.name.isEmpty()) { + push(u"REQ_INFO_NAME_INVALID"_q); + } + if (_invoice.isEmailRequested && information.email.isEmpty()) { + push(u"REQ_INFO_EMAIL_INVALID"_q); + } + if (_invoice.isPhoneRequested && information.phone.isEmpty()) { + push(u"REQ_INFO_PHONE_INVALID"_q); + } + if (!errors.isEmpty()) { + return Error{ Error::Type::Validate, errors.front() }; } return Error(); } @@ -442,6 +472,9 @@ Error Form::localInformationError( void Form::validateCard(const Ui::UncheckedCardDetails &details) { Expects(!v::is_null(_paymentMethod.native.data)); + if (!validateCardLocal(details)) { + return; + } const auto &native = _paymentMethod.native.data; if (const auto stripe = std::get_if(&native)) { validateCard(*stripe, details); @@ -450,6 +483,52 @@ void Form::validateCard(const Ui::UncheckedCardDetails &details) { } } +bool Form::validateCardLocal(const Ui::UncheckedCardDetails &details) const { + if (auto error = cardErrorLocal(details)) { + _updates.fire(std::move(error)); + return false; + } + return true; +} + +Error Form::cardErrorLocal(const Ui::UncheckedCardDetails &details) const { + using namespace Stripe; + + auto errors = QStringList(); + const auto push = [&](const QString &id) { + errors.push_back(id); + }; + const auto kValid = ValidationState::Valid; + if (ValidateCard(details.number).state != kValid) { + push(u"LOCAL_CARD_NUMBER_INVALID"_q); + } + if (ValidateParsedExpireDate( + details.expireMonth, + details.expireYear + ) != kValid) { + push(u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q); + } + if (ValidateCvc(details.number, details.cvc).state != kValid) { + push(u"LOCAL_CARD_CVC_INVALID"_q); + } + if (_paymentMethod.ui.native.needCardholderName + && details.cardholderName.isEmpty()) { + push(u"LOCAL_CARD_HOLDER_NAME_INVALID"_q); + } + if (_paymentMethod.ui.native.needCountry + && details.addressCountry.isEmpty()) { + push(u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q); + } + if (_paymentMethod.ui.native.needZip + && details.addressZip.isEmpty()) { + push(u"LOCAL_CARD_BILLING_ZIP_INVALID"_q); + } + if (!errors.isEmpty()) { + return Error{ Error::Type::Validate, errors.front() }; + } + return Error(); +} + void Form::validateCard( const StripePaymentMethod &method, const Ui::UncheckedCardDetails &details) { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 367512e2b..c964a6683 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -196,14 +196,23 @@ private: void fillPaymentMethodInformation(); void fillStripeNativeMethod(); void refreshPaymentMethodDetails(); + [[nodiscard]] QString defaultPhone() const; [[nodiscard]] QString defaultCountry() const; void validateCard( const StripePaymentMethod &method, const Ui::UncheckedCardDetails &details); - [[nodiscard]] Error localInformationError( + bool validateInformationLocal( const Ui::RequestedInformation &information) const; + [[nodiscard]] Error informationErrorLocal( + const Ui::RequestedInformation &information) const; + + bool validateCardLocal( + const Ui::UncheckedCardDetails &details) const; + [[nodiscard]] Error cardErrorLocal( + const Ui::UncheckedCardDetails &details) const; + const not_null _session; MTP::Sender _api; diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card.h b/Telegram/SourceFiles/payments/stripe/stripe_card.h index 0b9059262..30ff47b64 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card.h +++ b/Telegram/SourceFiles/payments/stripe/stripe_card.h @@ -20,6 +20,7 @@ enum class CardBrand { Discover, JCB, DinersClub, + UnionPay, Unknown, }; diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp new file mode 100644 index 000000000..aa2482c09 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp @@ -0,0 +1,279 @@ +/* +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 "stripe/stripe_card_validator.h" + +#include + +namespace Stripe { +namespace { + +constexpr auto kMinCvcLength = 3; + +struct BinRange { + QString low; + QString high; + int length = 0; + CardBrand brand = CardBrand::Unknown; +}; + +[[nodiscard]] const std::vector &AllRanges() { + static auto kResult = std::vector{ + // Unknown + { "", "", 19, CardBrand::Unknown }, + // American Express + { "34", "34", 15, CardBrand::Amex }, + { "37", "37", 15, CardBrand::Amex }, + // Diners Club + { "30", "30", 16, CardBrand::DinersClub }, + { "36", "36", 14, CardBrand::DinersClub }, + { "38", "39", 16, CardBrand::DinersClub }, + // Discover + { "60", "60", 16, CardBrand::Discover }, + { "64", "65", 16, CardBrand::Discover }, + // JCB + { "35", "35", 16, CardBrand::JCB }, + // Mastercard + { "50", "59", 16, CardBrand::MasterCard }, + { "22", "27", 16, CardBrand::MasterCard }, + { "67", "67", 16, CardBrand::MasterCard }, // Maestro + // UnionPay + { "62", "62", 16, CardBrand::UnionPay }, + { "81", "81", 16, CardBrand::UnionPay }, + // Visa + { "40", "49", 16, CardBrand::Visa }, + { "413600", "413600", 13, CardBrand::Visa }, + { "444509", "444509", 13, CardBrand::Visa }, + { "444509", "444509", 13, CardBrand::Visa }, + { "444550", "444550", 13, CardBrand::Visa }, + { "450603", "450603", 13, CardBrand::Visa }, + { "450617", "450617", 13, CardBrand::Visa }, + { "450628", "450629", 13, CardBrand::Visa }, + { "450636", "450636", 13, CardBrand::Visa }, + { "450640", "450641", 13, CardBrand::Visa }, + { "450662", "450662", 13, CardBrand::Visa }, + { "463100", "463100", 13, CardBrand::Visa }, + { "476142", "476142", 13, CardBrand::Visa }, + { "476143", "476143", 13, CardBrand::Visa }, + { "492901", "492902", 13, CardBrand::Visa }, + { "492920", "492920", 13, CardBrand::Visa }, + { "492923", "492923", 13, CardBrand::Visa }, + { "492928", "492930", 13, CardBrand::Visa }, + { "492937", "492937", 13, CardBrand::Visa }, + { "492939", "492939", 13, CardBrand::Visa }, + { "492960", "492960", 13, CardBrand::Visa }, + }; + return kResult; +} + +[[nodiscard]] bool BinRangeMatchesNumber( + const BinRange &range, + const QString &sanitized) { + const auto minWithLow = std::min(sanitized.size(), range.low.size()); + if (sanitized.midRef(0, minWithLow).toInt() + < range.low.midRef(0, minWithLow).toInt()) { + return false; + } + const auto minWithHigh = std::min(sanitized.size(), range.high.size()); + if (sanitized.midRef(0, minWithHigh).toInt() + > range.high.midRef(0, minWithHigh).toInt()) { + return false; + } + return true; +} + +[[nodiscard]] bool IsNumeric(const QString &value) { + return QRegularExpression("^[0-9]*$").match(value).hasMatch(); +} + +[[nodiscard]] QString RemoveWhitespaces(QString value) { + return value.replace(QRegularExpression("\\s"), QString()); +} + +[[nodiscard]] std::vector BinRangesForNumber( + const QString &sanitized) { + const auto &all = AllRanges(); + auto result = std::vector(); + result.reserve(all.size()); + for (const auto &range : all) { + if (BinRangeMatchesNumber(range, sanitized)) { + result.push_back(range); + } + } + return result; +} + +[[nodiscard]] BinRange MostSpecificBinRangeForNumber( + const QString &sanitized) { + auto possible = BinRangesForNumber(sanitized); + const auto compare = [&](const BinRange &a, const BinRange &b) { + if (sanitized.isEmpty()) { + const auto aUnknown = (a.brand == CardBrand::Unknown); + const auto bUnknown = (b.brand == CardBrand::Unknown); + if (aUnknown && !bUnknown) { + return true; + } else if (!aUnknown && bUnknown) { + return false; + } + } + return a.low.size() < b.low.size(); + }; + std::sort(begin(possible), end(possible), compare); + return possible.back(); +} + +[[nodiscard]] int MaxCvcLengthForBranch(CardBrand brand) { + switch (brand) { + case CardBrand::Amex: + case CardBrand::Unknown: + return 4; + default: + return 3; + } +} + +[[nodiscard]] std::vector PossibleBrandsForNumber( + const QString &sanitized) { + const auto ranges = BinRangesForNumber(sanitized); + auto result = std::vector(); + for (const auto &range : ranges) { + const auto brand = range.brand; + if (brand == CardBrand::Unknown + || (std::find(begin(result), end(result), brand) + != end(result))) { + continue; + } + result.push_back(brand); + } + return result; +} + +[[nodiscard]] CardBrand BrandForNumber(const QString &number) { + const auto sanitized = RemoveWhitespaces(number); + if (!IsNumeric(sanitized)) { + return CardBrand::Unknown; + } + const auto possible = PossibleBrandsForNumber(sanitized); + return (possible.size() == 1) ? possible.front() : CardBrand::Unknown; +} + +[[nodiscard]] bool IsValidLuhn(const QString &sanitized) { + auto odd = true; + auto sum = 0; + for (auto i = sanitized.end(); i != sanitized.begin();) { + --i; + auto digit = int(i->unicode() - '0'); + odd = !odd; + if (odd) { + digit *= 2; + } + if (digit > 9) { + digit -= 9; + } + sum += digit; + } + return (sum % 10) == 0; +} + +} // namespace + +CardValidationResult ValidateCard(const QString &number) { + const auto sanitized = RemoveWhitespaces(number); + if (!IsNumeric(sanitized)) { + return { .state = ValidationState::Invalid }; + } else if (sanitized.isEmpty()) { + return { .state = ValidationState::Incomplete }; + } + const auto range = MostSpecificBinRangeForNumber(sanitized); + const auto brand = range.brand; + if (sanitized.size() > range.length) { + return { .state = ValidationState::Invalid, .brand = brand }; + } else if (sanitized.size() < range.length) { + return { .state = ValidationState::Incomplete, .brand = brand }; + } else if (!IsValidLuhn(sanitized)) { + return { .state = ValidationState::Invalid, .brand = brand }; + } + return { + .state = ValidationState::Valid, + .brand = brand, + .finished = true, + }; +} + +ExpireDateValidationResult ValidateExpireDate(const QString &date) { + const auto sanitized = RemoveWhitespaces(date).replace('/', QString()); + if (!IsNumeric(sanitized)) { + return { ValidationState::Invalid }; + } else if (sanitized.size() < 2) { + return { ValidationState::Incomplete }; + } + const auto normalized = (sanitized[0] > '1' ? "0" : "") + sanitized; + const auto month = normalized.mid(0, 2).toInt(); + if (month < 1 || month > 12) { + return { ValidationState::Invalid }; + } else if (normalized.size() < 4) { + return { ValidationState::Incomplete }; + } else if (normalized.size() > 4) { + return { ValidationState::Invalid }; + } + const auto year = 2000 + normalized.mid(2).toInt(); + + const auto currentDate = QDate::currentDate(); + const auto currentMonth = currentDate.month(); + const auto currentYear = currentDate.year(); + if (year < currentYear) { + return { ValidationState::Invalid }; + } else if (year == currentYear && month < currentMonth) { + return { ValidationState::Invalid }; + } + return { ValidationState::Valid, true }; +} + +ValidationState ValidateParsedExpireDate( + quint32 month, + quint32 year) { + if ((year / 100) != 20) { + return ValidationState::Invalid; + } + return ValidateExpireDate( + QString("%1%2" + ).arg(month, 2, 10, QChar('0') + ).arg(year % 100, 2, 10, QChar('0')) + ).state; +} + +CvcValidationResult ValidateCvc( + const QString &number, + const QString &cvc) { + if (!IsNumeric(cvc)) { + return { ValidationState::Invalid }; + } else if (cvc.size() < kMinCvcLength) { + return { ValidationState::Incomplete }; + } + const auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number)); + if (cvc.size() > maxLength) { + return { ValidationState::Invalid }; + } + return { ValidationState::Valid, (cvc.size() == maxLength) }; +} + +std::vector CardNumberFormat(const QString &number) { + static const auto kDefault = std::vector{ 4, 4, 4, 4 }; + const auto sanitized = RemoveWhitespaces(number); + if (!IsNumeric(sanitized)) { + return kDefault; + } + const auto range = MostSpecificBinRangeForNumber(sanitized); + if (range.brand == CardBrand::DinersClub && range.length == 14) { + return { 4, 6, 4 }; + } else if (range.brand == CardBrand::Amex) { + return { 4, 6, 5 }; + } + return kDefault; +} + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_validator.h b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.h new file mode 100644 index 000000000..417d4d958 --- /dev/null +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.h @@ -0,0 +1,51 @@ +/* +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 "stripe/stripe_card.h" + +namespace Stripe { + +enum class ValidationState { + Invalid, + Incomplete, + Valid, +}; + +struct CardValidationResult { + ValidationState state = ValidationState::Invalid; + CardBrand brand = CardBrand::Unknown; + bool finished = false; +}; + +[[nodiscard]] CardValidationResult ValidateCard(const QString &number); + +struct ExpireDateValidationResult { + ValidationState state = ValidationState::Invalid; + bool finished = false; +}; + +[[nodiscard]] ExpireDateValidationResult ValidateExpireDate( + const QString &date); + +[[nodiscard]] ValidationState ValidateParsedExpireDate( + quint32 month, + quint32 year); + +struct CvcValidationResult { + ValidationState state = ValidationState::Invalid; + bool finished = false; +}; + +[[nodiscard]] CvcValidationResult ValidateCvc( + const QString &number, + const QString &cvc); + +[[nodiscard]] std::vector CardNumberFormat(const QString &number); + +} // namespace Stripe diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 0d25d4995..583e73d10 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_panel_delegate.h" #include "payments/ui/payments_field.h" +#include "stripe/stripe_card_validator.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -18,10 +19,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_payments.h" #include "styles/style_passport.h" +#include + namespace Payments::Ui { namespace { -constexpr auto kMaxPostcodeSize = 10; +struct SimpleFieldState { + QString value; + int position = 0; +}; [[nodiscard]] uint32 ExtractYear(const QString &value) { return value.split('/').value(1).toInt() + 2000; @@ -31,6 +37,165 @@ constexpr auto kMaxPostcodeSize = 10; return value.split('/').value(0).toInt(); } +[[nodiscard]] QString RemoveNonNumbers(QString value) { + return value.replace(QRegularExpression("[^0-9]"), QString()); +} + +[[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) { + return { + .value = RemoveNonNumbers(state.value), + .position = RemoveNonNumbers( + state.value.mid(0, state.position)).size(), + }; +} + +[[nodiscard]] SimpleFieldState PostprocessCardValidateResult( + SimpleFieldState result) { + const auto groups = Stripe::CardNumberFormat(result.value); + auto position = 0; + for (const auto length : groups) { + position += length; + if (position >= result.value.size()) { + break; + } + result.value.insert(position, QChar(' ')); + if (result.position >= position) { + ++result.position; + } + ++position; + } + return result; +} + +[[nodiscard]] SimpleFieldState PostprocessExpireDateValidateResult( + SimpleFieldState result) { + if (result.value.isEmpty()) { + return result; + } else if (result.value[0] == '1' && result.value[1] > '2') { + result.value = result.value.mid(0, 2); + return result; + } else if (result.value[0] > '1') { + result.value = '0' + result.value; + ++result.position; + } + if (result.value.size() > 1) { + if (result.value.size() > 4) { + result.value = result.value.mid(0, 4); + } + result.value.insert(2, '/'); + if (result.position >= 2) { + ++result.position; + } + } + return result; +} + +[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) { + return (request.wasAnchor == request.wasPosition) + && (request.wasPosition == request.nowPosition + 1) + && (request.wasValue.midRef(0, request.wasPosition - 1) + == request.nowValue.midRef(0, request.nowPosition)) + && (request.wasValue.midRef(request.wasPosition) + == request.nowValue.midRef(request.nowPosition)); +} + +[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) { + return (request.wasAnchor == request.wasPosition) + && (request.wasPosition == request.nowPosition) + && (request.wasValue.midRef(0, request.wasPosition) + == request.nowValue.midRef(0, request.nowPosition)) + && (request.wasValue.midRef(request.wasPosition + 1) + == request.nowValue.midRef(request.nowPosition)); +} + +template < + typename ValueValidator, + typename ValueValidateResult = decltype( + std::declval()(QString()))> +[[nodiscard]] auto ComplexNumberValidator( + ValueValidator valueValidator, + Fn postprocess) { + using namespace Stripe; + return [=](FieldValidateRequest request) { + const auto realNowState = [&] { + const auto backspaced = IsBackspace(request); + const auto deleted = IsDelete(request); + if (!backspaced && !deleted) { + return NumbersOnlyState({ + .value = request.nowValue, + .position = request.nowPosition, + }); + } + const auto realWasState = NumbersOnlyState({ + .value = request.wasValue, + .position = request.wasPosition, + }); + const auto changedValue = deleted + ? (realWasState.value.mid(0, realWasState.position) + + realWasState.value.mid(realWasState.position + 1)) + : (realWasState.position > 1) + ? (realWasState.value.mid(0, realWasState.position - 1) + + realWasState.value.mid(realWasState.position)) + : realWasState.value.mid(realWasState.position); + return SimpleFieldState{ + .value = changedValue, + .position = (deleted + ? realWasState.position + : std::max(realWasState.position - 1, 0)) + }; + }(); + const auto result = valueValidator(realNowState.value); + const auto postprocessed = postprocess(realNowState); + return FieldValidateResult{ + .value = postprocessed.value, + .position = postprocessed.position, + .invalid = (result.state == ValidationState::Invalid), + .finished = result.finished, + }; + }; + +} + +[[nodiscard]] auto CardNumberValidator() { + return ComplexNumberValidator( + Stripe::ValidateCard, + PostprocessCardValidateResult); +} + +[[nodiscard]] auto ExpireDateValidator() { + return ComplexNumberValidator( + Stripe::ValidateExpireDate, + PostprocessExpireDateValidateResult); +} + +[[nodiscard]] auto CvcValidator(Fn number) { + using namespace Stripe; + return [=](FieldValidateRequest request) { + const auto realNowState = NumbersOnlyState({ + .value = request.nowValue, + .position = request.nowPosition, + }); + const auto result = ValidateCvc(number(), realNowState.value); + + return FieldValidateResult{ + .value = realNowState.value, + .position = realNowState.position, + .invalid = (result.state == ValidationState::Invalid), + .finished = result.finished, + }; + }; +} + +[[nodiscard]] auto CardHolderNameValidator() { + return [=](FieldValidateRequest request) { + return FieldValidateResult{ + .value = request.nowValue.toUpper(), + .position = request.nowPosition, + .invalid = request.nowValue.isEmpty(), + }; + }; +} + } // namespace EditCard::EditCard( @@ -111,15 +276,8 @@ not_null EditCard::setupContent() { _number = add({ .type = FieldType::CardNumber, .placeholder = tr::lng_payments_card_number(), - .required = true, + .validator = CardNumberValidator(), }); - if (_native.needCardholderName) { - _name = add({ - .type = FieldType::CardNumber, - .placeholder = tr::lng_payments_card_holder(), - .required = true, - }); - } auto container = inner->add( object_ptr( inner, @@ -128,12 +286,12 @@ not_null EditCard::setupContent() { _expire = std::make_unique(container, FieldConfig{ .type = FieldType::CardExpireDate, .placeholder = rpl::single(u"MM / YY"_q), - .required = true, + .validator = ExpireDateValidator(), }); _cvc = std::make_unique(container, FieldConfig{ .type = FieldType::CardCVC, .placeholder = rpl::single(u"CVC"_q), - .required = true, + .validator = CvcValidator([=] { return _number->value(); }), }); container->widthValue( ) | rpl::start_with_next([=](int width) { @@ -144,6 +302,24 @@ not_null EditCard::setupContent() { _expire->widget()->moveToLeft(0, 0, width); _cvc->widget()->moveToRight(0, 0, width); }, container->lifetime()); + + if (_native.needCardholderName) { + _name = add({ + .type = FieldType::CardNumber, + .placeholder = tr::lng_payments_card_holder(), + .validator = CardHolderNameValidator(), + }); + } + + _number->setNextField(_expire.get()); + _expire->setPreviousField(_number.get()); + _expire->setNextField(_cvc.get()); + _cvc->setPreviousField(_expire.get()); + if (_name) { + _cvc->setNextField(_name.get()); + _name->setPreviousField(_cvc.get()); + } + if (_native.needCountry || _native.needZip) { inner->add( object_ptr( @@ -156,18 +332,23 @@ not_null EditCard::setupContent() { _country = add({ .type = FieldType::Country, .placeholder = tr::lng_payments_billing_country(), + .validator = RequiredFinishedValidator(), .showBox = showBox, .defaultCountry = _native.defaultCountry, - .required = true, }); } if (_native.needZip) { _zip = add({ .type = FieldType::Text, .placeholder = tr::lng_payments_billing_zip_code(), - .maxLength = kMaxPostcodeSize, - .required = true, + .validator = RequiredValidator(), }); + if (_country) { + _country->finished( + ) | rpl::start_with_next([=] { + _zip->setFocus(); + }, lifetime()); + } } return inner; } @@ -198,7 +379,7 @@ void EditCard::updateControlsGeometry() { auto EditCard::lookupField(CardField field) const -> Field* { switch (field) { case CardField::Number: return _number.get(); - case CardField::CVC: return _cvc.get(); + case CardField::Cvc: return _cvc.get(); case CardField::ExpireDate: return _expire.get(); case CardField::Name: return _name.get(); case CardField::AddressCountry: return _country.get(); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 522878202..d9b4f2562 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -26,6 +26,8 @@ constexpr auto kMaxPostcodeSize = 10; constexpr auto kMaxNameSize = 64; constexpr auto kMaxEmailSize = 128; constexpr auto kMaxPhoneSize = 16; +constexpr auto kMinCitySize = 2; +constexpr auto kMaxCitySize = 64; } // namespace @@ -112,18 +114,17 @@ not_null EditInformation::setupContent() { _street1 = add({ .placeholder = tr::lng_payments_address_street1(), .value = _information.shippingAddress.address1, - .maxLength = kMaxStreetSize, - .required = true, + .validator = RangeLengthValidator(1, kMaxStreetSize), }); _street2 = add({ .placeholder = tr::lng_payments_address_street2(), .value = _information.shippingAddress.address2, - .maxLength = kMaxStreetSize, + .validator = MaxLengthValidator(kMaxStreetSize), }); _city = add({ .placeholder = tr::lng_payments_address_city(), .value = _information.shippingAddress.city, - .required = true, + .validator = RangeLengthValidator(kMinCitySize, kMaxCitySize), }); _state = add({ .placeholder = tr::lng_payments_address_state(), @@ -133,44 +134,38 @@ not_null EditInformation::setupContent() { .type = FieldType::Country, .placeholder = tr::lng_payments_address_country(), .value = _information.shippingAddress.countryIso2, + .validator = RequiredFinishedValidator(), .showBox = showBox, .defaultCountry = _information.defaultCountry, - .required = true, }); _postcode = add({ .placeholder = tr::lng_payments_address_postcode(), .value = _information.shippingAddress.postcode, - .maxLength = kMaxPostcodeSize, - .required = true, + .validator = RangeLengthValidator(1, kMaxPostcodeSize), }); - //StreetValidate, // #TODO payments - //CityValidate, - //CountryValidate, - //CountryFormat, - //PostcodeValidate, } if (_invoice.isNameRequested) { _name = add({ .placeholder = tr::lng_payments_info_name(), .value = _information.name, - .maxLength = kMaxNameSize, - .required = true, + .validator = RangeLengthValidator(1, kMaxNameSize), }); } if (_invoice.isEmailRequested) { _email = add({ + .type = FieldType::Email, .placeholder = tr::lng_payments_info_email(), .value = _information.email, - .maxLength = kMaxEmailSize, - .required = true, + .validator = RangeLengthValidator(1, kMaxEmailSize), }); } if (_invoice.isPhoneRequested) { _phone = add({ + .type = FieldType::Phone, .placeholder = tr::lng_payments_info_phone(), .value = _information.phone, - .maxLength = kMaxPhoneSize, - .required = true, + .validator = RangeLengthValidator(1, kMaxPhoneSize), + .defaultPhone = _information.defaultPhone, }); } return inner; @@ -215,6 +210,8 @@ auto EditInformation::lookupField(InformationField field) const -> Field* { RequestedInformation EditInformation::collect() const { return { + .defaultPhone = _information.defaultPhone, + .defaultCountry = _information.defaultCountry, .name = _name ? _name->value() : QString(), .phone = _phone ? _phone->value() : QString(), .email = _email ? _email->value() : QString(), diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 0fc86222a..fe958b207 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -9,8 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/boxes/country_select_box.h" +#include "ui/ui_utility.h" +#include "ui/special_fields.h" #include "data/data_countries.h" #include "base/platform/base_platform_info.h" +#include "base/event_filter.h" #include "styles/style_payments.h" namespace Payments::Ui { @@ -91,12 +94,18 @@ namespace { case FieldType::CardExpireDate: case FieldType::CardCVC: case FieldType::Country: - case FieldType::Phone: return CreateChild( wrap.get(), st::paymentsField, std::move(config.placeholder), Parse(config)); + case FieldType::Phone: + return CreateChild( + wrap.get(), + st::paymentsField, + std::move(config.placeholder), + ExtractPhonePrefix(config.defaultPhone), + Parse(config)); } Unexpected("FieldType in Payments::Ui::LookupMaskedField."); } @@ -115,6 +124,10 @@ Field::Field(QWidget *parent, FieldConfig &&config) if (_config.type == FieldType::Country) { setupCountry(); } + if (const auto &validator = config.validator) { + setupValidator(validator); + } + setupFrontBackspace(); } RpWidget *Field::widget() const { @@ -132,6 +145,14 @@ QString Field::value() const { _countryIso2); } +rpl::producer<> Field::frontBackspace() const { + return _frontBackspace.events(); +} + +rpl::producer<> Field::finished() const { + return _finished.events(); +} + void Field::setupMaskedGeometry() { Expects(_masked != nullptr); @@ -168,13 +189,136 @@ void Field::setupCountry() { _countryIso2 = iso2; _masked->setText(Data::CountryNameByISO2(iso2)); _masked->hideError(); - setFocus(); raw->closeBox(); }, _masked->lifetime()); + raw->boxClosing() | rpl::start_with_next([=] { + setFocus(); + }, _masked->lifetime()); _config.showBox(std::move(box)); }); } +void Field::setupValidator(Fn validator) { + Expects(validator != nullptr); + + const auto state = [=]() -> State { + if (_masked) { + const auto position = _masked->cursorPosition(); + const auto selectionStart = _masked->selectionStart(); + const auto selectionEnd = _masked->selectionEnd(); + return { + .value = value(), + .position = position, + .anchor = (selectionStart == selectionEnd + ? position + : (selectionStart == position) + ? selectionEnd + : selectionStart), + }; + } + const auto cursor = _input->textCursor(); + return { + .value = value(), + .position = cursor.position(), + .anchor = cursor.anchor(), + }; + }; + const auto save = [=] { + _was = state(); + }; + const auto setText = [=](const QString &text) { + if (_masked) { + _masked->setText(text); + } else { + _input->setText(text); + } + }; + const auto setPosition = [=](int position) { + if (_masked) { + _masked->setCursorPosition(position); + } else { + auto cursor = _input->textCursor(); + cursor.setPosition(position); + _input->setTextCursor(cursor); + } + }; + const auto validate = [=] { + if (_validating) { + return; + } + _validating = true; + const auto guard = gsl::finally([&] { + _validating = false; + save(); + }); + + const auto now = state(); + const auto result = validator(ValidateRequest{ + .wasValue = _was.value, + .wasPosition = _was.position, + .wasAnchor = _was.anchor, + .nowValue = now.value, + .nowPosition = now.position, + }); + const auto changed = (result.value != now.value); + if (changed) { + setText(result.value); + } + if (changed || result.position != now.position) { + setPosition(result.position); + } + if (result.finished) { + _finished.fire({}); + } else if (result.invalid) { + Ui::PostponeCall( + _masked ? (QWidget*)_masked : _input, + [=] { showErrorNoFocus(); }); + } + }; + if (_masked) { + QObject::connect(_masked, &QLineEdit::cursorPositionChanged, save); + QObject::connect(_masked, &MaskedInputField::changed, validate); + } else { + const auto raw = _input->rawTextEdit(); + QObject::connect(raw, &QTextEdit::cursorPositionChanged, save); + QObject::connect(_input, &InputField::changed, validate); + } +} + +void Field::setupFrontBackspace() { + const auto filter = [=](not_null e) { + const auto frontBackspace = (e->type() == QEvent::KeyPress) + && (static_cast(e.get())->key() == Qt::Key_Backspace) + && (_masked + ? (_masked->cursorPosition() == 0 + && _masked->selectionLength() == 0) + : (_input->textCursor().position() == 0 + && _input->textCursor().anchor() == 0)); + if (frontBackspace) { + _frontBackspace.fire({}); + } + return base::EventFilterResult::Continue; + }; + if (_masked) { + base::install_event_filter(_masked, filter); + } else { + base::install_event_filter(_input->rawTextEdit(), filter); + } +} + +void Field::setNextField(not_null field) { + finished() | rpl::start_with_next([=] { + field->setFocus(); + }, _masked ? _masked->lifetime() : _input->lifetime()); +} + +void Field::setPreviousField(not_null field) { + frontBackspace( + ) | rpl::start_with_next([=] { + field->setFocus(); + }, _masked ? _masked->lifetime() : _input->lifetime()); +} + void Field::setFocus() { if (_config.type == FieldType::Country) { _wrap->setFocus(); @@ -206,4 +350,12 @@ void Field::showError() { } } +void Field::showErrorNoFocus() { + if (_input) { + _input->showErrorNoFocus(); + } else { + _masked->showErrorNoFocus(); + } +} + } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index c82b4d2fe..2061ff533 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -31,14 +31,59 @@ enum class FieldType { Email, }; +struct FieldValidateRequest { + QString wasValue; + int wasPosition = 0; + int wasAnchor = 0; + QString nowValue; + int nowPosition = 0; +}; + +struct FieldValidateResult { + QString value; + int position = 0; + bool invalid = false; + bool finished = false; +}; + +[[nodiscard]] auto RangeLengthValidator(int minLength, int maxLength) { + return [=](FieldValidateRequest request) { + return FieldValidateResult{ + .value = request.nowValue, + .position = request.nowPosition, + .invalid = (request.nowValue.size() < minLength + || request.nowValue.size() > maxLength), + }; + }; +} + +[[nodiscard]] auto MaxLengthValidator(int maxLength) { + return RangeLengthValidator(0, maxLength); +} + +[[nodiscard]] auto RequiredValidator() { + return RangeLengthValidator(1, std::numeric_limits::max()); +} + +[[nodiscard]] auto RequiredFinishedValidator() { + return [=](FieldValidateRequest request) { + return FieldValidateResult{ + .value = request.nowValue, + .position = request.nowPosition, + .invalid = request.nowValue.isEmpty(), + .finished = !request.nowValue.isEmpty(), + }; + }; +} + struct FieldConfig { FieldType type = FieldType::Text; rpl::producer placeholder; QString value; + Fn validator; Fn)> showBox; + QString defaultPhone; QString defaultCountry; - int maxLength = 0; - bool required = false; }; class Field final { @@ -49,20 +94,40 @@ public: [[nodiscard]] object_ptr ownedWidget() const; [[nodiscard]] QString value() const; + [[nodiscard]] rpl::producer<> frontBackspace() const; + [[nodiscard]] rpl::producer<> finished() const; void setFocus(); void setFocusFast(); void showError(); + void showErrorNoFocus(); + + void setNextField(not_null field); + void setPreviousField(not_null field); private: + struct State { + QString value; + int position = 0; + int anchor = 0; + }; + using ValidateRequest = FieldValidateRequest; + using ValidateResult = FieldValidateResult; + void setupMaskedGeometry(); void setupCountry(); + void setupValidator(Fn validator); + void setupFrontBackspace(); const FieldConfig _config; const base::unique_qptr _wrap; + rpl::event_stream<> _frontBackspace; + rpl::event_stream<> _finished; InputField *_input = nullptr; MaskedInputField *_masked = nullptr; QString _countryIso2; + State _was; + bool _validating = false; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 68c5965c6..aae6bd1af 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "ui/text/format_values.h" +#include "data/data_countries.h" #include "lang/lang_keys.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -267,7 +268,8 @@ void FormSummary::setupSections(not_null layout) { push(_information.shippingAddress.address2); push(_information.shippingAddress.city); push(_information.shippingAddress.state); - push(_information.shippingAddress.countryIso2); + push(Data::CountryNameByISO2( + _information.shippingAddress.countryIso2)); push(_information.shippingAddress.postcode); add( tr::lng_payments_shipping_address(), diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index cf97ac5c7..da920af39 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -87,6 +87,7 @@ struct Address { }; struct RequestedInformation { + QString defaultPhone; QString defaultCountry; QString name; @@ -144,7 +145,7 @@ struct PaymentMethodDetails { enum class CardField { Number, - CVC, + Cvc, ExpireDate, Name, AddressCountry, diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp index a4652d746..a31a84fc2 100644 --- a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp @@ -28,7 +28,7 @@ QString LastValidISO; class CountrySelectBox::Inner : public TWidget { public: - Inner(QWidget *parent, Type type); + Inner(QWidget *parent, const QString &iso, Type type); ~Inner(); void updateFilter(QString filter = QString()); @@ -93,10 +93,7 @@ CountrySelectBox::CountrySelectBox(QWidget*) CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) : _type(type) , _select(this, st::defaultMultiSelect, tr::lng_country_ph()) -, _ownedInner(this, type) { - if (Data::CountriesByISO2().contains(iso)) { - LastValidISO = iso; - } +, _ownedInner(this, iso, type) { } rpl::producer CountrySelectBox::countryChosen() const { @@ -169,7 +166,10 @@ void CountrySelectBox::setInnerFocus() { _select->setInnerFocus(); } -CountrySelectBox::Inner::Inner(QWidget *parent, Type type) +CountrySelectBox::Inner::Inner( + QWidget *parent, + const QString &iso, + Type type) : TWidget(parent) , _type(type) , _rowHeight(st::countryRowHeight) { @@ -177,6 +177,10 @@ CountrySelectBox::Inner::Inner(QWidget *parent, Type type) const auto &byISO2 = Data::CountriesByISO2(); + if (byISO2.contains(iso)) { + LastValidISO = iso; + } + _list.reserve(byISO2.size()); _namesList.reserve(byISO2.size()); diff --git a/Telegram/SourceFiles/ui/special_fields.cpp b/Telegram/SourceFiles/ui/special_fields.cpp index 7ed7f90a4..a1099e4d7 100644 --- a/Telegram/SourceFiles/ui/special_fields.cpp +++ b/Telegram/SourceFiles/ui/special_fields.cpp @@ -7,16 +7,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/special_fields.h" -#include "core/application.h" #include "lang/lang_keys.h" #include "data/data_countries.h" // Data::ValidPhoneCode #include "numbers.h" +#include + namespace Ui { namespace { constexpr auto kMaxUsernameLength = 32; +// Rest of the phone number, without country code (seen 12 at least), +// need more for service numbers. +constexpr auto kMaxPhoneTailLength = 32; + +// Max length of country phone code. +constexpr auto kMaxPhoneCodeLength = 4; + } // namespace CountryCodeInput::CountryCodeInput( @@ -130,7 +138,9 @@ void PhonePartInput::correctValue( ++digitCount; } } - if (digitCount > MaxPhoneTailLength) digitCount = MaxPhoneTailLength; + if (digitCount > kMaxPhoneTailLength) { + digitCount = kMaxPhoneTailLength; + } bool inPart = !_pattern.isEmpty(); int curPart = -1, leftInPart = 0; @@ -273,6 +283,14 @@ void UsernameInput::correctValue( setCorrectedText(now, nowCursor, now.mid(from, len), newPos); } +QString ExtractPhonePrefix(const QString &phone) { + const auto pattern = phoneNumberParse(phone); + if (!pattern.isEmpty()) { + return phone.mid(0, pattern[0]); + } + return QString(); +} + PhoneInput::PhoneInput( QWidget *parent, const style::InputField &st, @@ -324,7 +342,7 @@ void PhoneInput::correctValue( QString &now, int &nowCursor) { auto digits = now; - digits.replace(QRegularExpression(qsl("[^\\d]")), QString()); + digits.replace(QRegularExpression("[^\\d]"), QString()); _pattern = phoneNumberParse(digits); QString newPlaceholder; @@ -350,7 +368,7 @@ void PhoneInput::correctValue( } QString newText; - int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), MaxPhoneCodeLength + MaxPhoneTailLength); + int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), kMaxPhoneCodeLength + kMaxPhoneTailLength); bool inPart = !_pattern.isEmpty(), plusFound = false; int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0; diff --git a/Telegram/SourceFiles/ui/special_fields.h b/Telegram/SourceFiles/ui/special_fields.h index 487684c4d..c0d50b6d2 100644 --- a/Telegram/SourceFiles/ui/special_fields.h +++ b/Telegram/SourceFiles/ui/special_fields.h @@ -90,6 +90,8 @@ private: }; +[[nodiscard]] QString ExtractPhonePrefix(const QString &phone); + class PhoneInput : public MaskedInputField { public: PhoneInput( diff --git a/Telegram/cmake/lib_stripe.cmake b/Telegram/cmake/lib_stripe.cmake index 6785d983b..49b0f791a 100644 --- a/Telegram/cmake/lib_stripe.cmake +++ b/Telegram/cmake/lib_stripe.cmake @@ -21,6 +21,8 @@ PRIVATE stripe/stripe_card.h stripe/stripe_card_params.cpp stripe/stripe_card_params.h + stripe/stripe_card_validator.cpp + stripe/stripe_card_validator.h stripe/stripe_decode.cpp stripe/stripe_decode.h stripe/stripe_error.cpp diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 66b94f63f..75d522651 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -146,7 +146,9 @@ PRIVATE ui/cached_round_corners.h ui/grouped_layout.cpp ui/grouped_layout.h - + ui/special_fields.cpp + ui/special_fields.h + ui/ui_pch.h ) @@ -163,4 +165,5 @@ PUBLIC PRIVATE desktop-app::lib_ffmpeg desktop-app::lib_webview + desktop-app::lib_stripe ) From 1050447eed932bf1cb4d00dab4c8529fa71461ba Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 Mar 2021 18:56:26 +0400 Subject: [PATCH 023/127] Add phone format and validation in payments. --- .../SourceFiles/boxes/add_contact_box.cpp | 2 +- Telegram/SourceFiles/intro/intro_phone.cpp | 22 ++++++++++--- .../SourceFiles/payments/payments_form.cpp | 1 - Telegram/SourceFiles/ui/special_fields.cpp | 16 ++++----- Telegram/SourceFiles/ui/special_fields.h | 33 ++++++++++--------- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index a69e40b96..a1510ac9a 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -268,7 +268,7 @@ AddContactBox::AddContactBox( this, st::defaultInputField, tr::lng_contact_phone(), - ExtractPhonePrefix(session->user()->phone()), + Ui::ExtractPhonePrefix(session->user()->phone()), phone) , _invertOrder(langFirstNameGoesSecond()) { if (!phone.isEmpty()) { diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 3f61abb39..d4260e460 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -46,12 +46,24 @@ PhoneWidget::PhoneWidget( , _code(this, st::introCountryCode) , _phone(this, st::introPhone) , _checkRequestTimer([=] { checkRequest(); }) { - connect(_phone, SIGNAL(voidBackspace(QKeyEvent*)), _code, SLOT(startErasing(QKeyEvent*))); - connect(_country, SIGNAL(codeChanged(const QString &)), _code, SLOT(codeSelected(const QString &))); - connect(_code, SIGNAL(codeChanged(const QString &)), _country, SLOT(onChooseCode(const QString &))); - connect(_code, SIGNAL(codeChanged(const QString &)), _phone, SLOT(onChooseCode(const QString &))); + _phone->frontBackspaceEvent( + ) | rpl::start_with_next([=](not_null e) { + _code->startErasing(e); + }, _code->lifetime()); + + connect(_country, &CountryInput::codeChanged, [=](const QString &code) { + _code->codeSelected(code); + }); + _code->codeChanged( + ) | rpl::start_with_next([=](const QString &code) { + _country->onChooseCode(code); + _phone->chooseCode(code); + }, _code->lifetime()); connect(_country, SIGNAL(codeChanged(const QString &)), _phone, SLOT(onChooseCode(const QString &))); - connect(_code, SIGNAL(addedToNumber(const QString &)), _phone, SLOT(addedToNumber(const QString &))); + _code->addedToNumber( + ) | rpl::start_with_next([=](const QString &added) { + _phone->addedToNumber(added); + }, _phone->lifetime()); connect(_phone, &Ui::PhonePartInput::changed, [=] { phoneChanged(); }); connect(_code, &Ui::CountryCodeInput::changed, [=] { phoneChanged(); }); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 96dfa27c1..3cc62685e 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -295,7 +295,6 @@ void Form::refreshPaymentMethodDetails() { const auto &entered = _paymentMethod.newCredentials; _paymentMethod.ui.title = entered ? entered.title : saved.title; _paymentMethod.ui.ready = entered || saved; - _paymentMethod.ui.native.defaultPhone = defaultPhone(); _paymentMethod.ui.native.defaultCountry = defaultCountry(); } diff --git a/Telegram/SourceFiles/ui/special_fields.cpp b/Telegram/SourceFiles/ui/special_fields.cpp index a1099e4d7..238bb6f06 100644 --- a/Telegram/SourceFiles/ui/special_fields.cpp +++ b/Telegram/SourceFiles/ui/special_fields.cpp @@ -30,8 +30,7 @@ constexpr auto kMaxPhoneCodeLength = 4; CountryCodeInput::CountryCodeInput( QWidget *parent, const style::InputField &st) -: MaskedInputField(parent, st) -, _nosignal(false) { +: MaskedInputField(parent, st) { } void CountryCodeInput::startErasing(QKeyEvent *e) { @@ -91,14 +90,15 @@ void CountryCodeInput::correctValue( setCorrectedText(now, nowCursor, newText, newPos); if (!_nosignal && was != newText) { - codeChanged(newText.mid(1)); + _codeChanged.fire(newText.mid(1)); } if (!addToNumber.isEmpty()) { - addedToNumber(addToNumber); + _addedToNumber.fire_copy(addToNumber); } } -PhonePartInput::PhonePartInput(QWidget *parent, const style::InputField &st) : MaskedInputField(parent, st/*, tr::lng_phone_ph(tr::now)*/) { +PhonePartInput::PhonePartInput(QWidget *parent, const style::InputField &st) +: MaskedInputField(parent, st/*, tr::lng_phone_ph(tr::now)*/) { } void PhonePartInput::paintAdditionalPlaceholder(Painter &p) { @@ -119,8 +119,8 @@ void PhonePartInput::paintAdditionalPlaceholder(Painter &p) { } void PhonePartInput::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Backspace && getLastText().isEmpty()) { - voidBackspace(e); + if (e->key() == Qt::Key_Backspace && cursorPosition() == 0) { + _frontBackspaceEvent.fire_copy(e); } else { MaskedInputField::keyPressEvent(e); } @@ -204,7 +204,7 @@ void PhonePartInput::addedToNumber(const QString &added) { startPlaceholderAnimation(); } -void PhonePartInput::onChooseCode(const QString &code) { +void PhonePartInput::chooseCode(const QString &code) { _pattern = phoneNumberParse(code); if (!_pattern.isEmpty() && _pattern.at(0) == code.size()) { _pattern.pop_front(); diff --git a/Telegram/SourceFiles/ui/special_fields.h b/Telegram/SourceFiles/ui/special_fields.h index c0d50b6d2..fae5093a4 100644 --- a/Telegram/SourceFiles/ui/special_fields.h +++ b/Telegram/SourceFiles/ui/special_fields.h @@ -12,18 +12,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class CountryCodeInput : public MaskedInputField { - Q_OBJECT - public: CountryCodeInput(QWidget *parent, const style::InputField &st); -public Q_SLOTS: void startErasing(QKeyEvent *e); - void codeSelected(const QString &code); -Q_SIGNALS: - void codeChanged(const QString &code); - void addedToNumber(const QString &added); + [[nodiscard]] rpl::producer addedToNumber() const { + return _addedToNumber.events(); + } + [[nodiscard]] rpl::producer codeChanged() const { + return _codeChanged.events(); + } + + void codeSelected(const QString &code); protected: void correctValue( @@ -33,22 +34,23 @@ protected: int &nowCursor) override; private: - bool _nosignal; + bool _nosignal = false; + rpl::event_stream _addedToNumber; + rpl::event_stream _codeChanged; }; class PhonePartInput : public MaskedInputField { - Q_OBJECT - public: PhonePartInput(QWidget *parent, const style::InputField &st); -public Q_SLOTS: - void addedToNumber(const QString &added); - void onChooseCode(const QString &code); + [[nodiscard]] auto frontBackspaceEvent() const + -> rpl::producer> { + return _frontBackspaceEvent.events(); + } -Q_SIGNALS: - void voidBackspace(QKeyEvent *e); + void addedToNumber(const QString &added); + void chooseCode(const QString &code); protected: void keyPressEvent(QKeyEvent *e) override; @@ -63,6 +65,7 @@ protected: private: QVector _pattern; QString _additionalPlaceholder; + rpl::event_stream> _frontBackspaceEvent; }; From 320adcd3893840550a23332da68b0be272608bbb Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 Mar 2021 19:53:55 +0400 Subject: [PATCH 024/127] Fix showing comments from the beginning. --- .../SourceFiles/history/history_message.cpp | 14 +++++++++++-- .../window/window_session_controller.cpp | 20 ++++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index 6e01d5681..c79bc2ae6 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -857,7 +857,12 @@ MsgId HistoryMessage::computeRepliesInboxReadTillFull() const { ? history()->owner().historyLoaded( peerFromChannel(views->commentsMegagroupId)) : history().get(); - return group ? std::max(local, group->inboxReadTillId()) : local; + if (const auto megagroup = group->peer->asChannel()) { + if (megagroup->amIn()) { + return std::max(local, group->inboxReadTillId()); + } + } + return local; } MsgId HistoryMessage::repliesOutboxReadTill() const { @@ -891,7 +896,12 @@ MsgId HistoryMessage::computeRepliesOutboxReadTillFull() const { ? history()->owner().historyLoaded( peerFromChannel(views->commentsMegagroupId)) : history().get(); - return group ? std::max(local, group->outboxReadTillId()) : local; + if (const auto megagroup = group->peer->asChannel()) { + if (megagroup->amIn()) { + return std::max(local, group->outboxReadTillId()); + } + } + return local; } void HistoryMessage::setRepliesMaxId(MsgId maxId) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index ecdf58d53..4d37527dd 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -354,24 +354,20 @@ void SessionNavigation::showRepliesForMessage( if (const auto maxId = data.vmax_id()) { item->setRepliesMaxId(maxId->v); } - if (const auto readTill = data.vread_inbox_max_id()) { - item->setRepliesInboxReadTill(readTill->v); - } - if (const auto readTill = data.vread_outbox_max_id()) { - item->setRepliesOutboxReadTill(readTill->v); - } + item->setRepliesInboxReadTill( + data.vread_inbox_max_id().value_or_empty()); + item->setRepliesOutboxReadTill( + data.vread_outbox_max_id().value_or_empty()); const auto post = _session->data().message(channelId, rootId); if (post && item->history()->channelId() != channelId) { post->setCommentsItemId(item->fullId()); if (const auto maxId = data.vmax_id()) { post->setRepliesMaxId(maxId->v); } - if (const auto readTill = data.vread_inbox_max_id()) { - post->setRepliesInboxReadTill(readTill->v); - } - if (const auto readTill = data.vread_outbox_max_id()) { - post->setRepliesOutboxReadTill(readTill->v); - } + post->setRepliesInboxReadTill( + data.vread_inbox_max_id().value_or_empty()); + post->setRepliesOutboxReadTill( + data.vread_outbox_max_id().value_or_empty()); } showSection(std::make_shared( item, From c7a1771dec7e015e959df82f3d9a3da56c0a4ee0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 10:01:31 +0400 Subject: [PATCH 025/127] Simple receipt viewing. --- Telegram/Resources/langs/lang.strings | 2 + .../payments/payments_checkout_process.cpp | 9 +-- .../SourceFiles/payments/payments_form.cpp | 70 ++++++++++++++++++- Telegram/SourceFiles/payments/payments_form.h | 4 ++ .../payments/ui/payments_form_summary.cpp | 34 ++++++--- .../payments/ui/payments_panel.cpp | 4 +- .../payments/ui/payments_panel_data.h | 15 ++++ 7 files changed, 120 insertions(+), 18 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3f49c3076..1366dd31d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1861,7 +1861,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_receipt_button" = "Receipt"; "lng_payments_checkout_title" = "Checkout"; +"lng_payments_receipt_title" = "Receipt"; "lng_payments_total_label" = "Total"; +"lng_payments_date_label" = "Date"; "lng_payments_pay_amount" = "Pay {amount}"; "lng_payments_payment_method" = "Payment Method"; "lng_payments_new_card" = "New Card..."; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 7d67fb94e..f5508fb7f 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -149,9 +149,7 @@ void CheckoutProcess::handleError(const Error &error) { const auto &id = error.id; switch (error.type) { case Error::Type::Form: - if (id == u"INVOICE_ALREADY_PAID"_q) { - showToast({ "Already Paid!" }); // #TODO payments errors message - } else if (true + if (true || id == u"PROVIDER_ACCOUNT_INVALID"_q || id == u"PROVIDER_ACCOUNT_TIMEOUT"_q) { showToast({ "Error: " + id }); @@ -232,8 +230,6 @@ void CheckoutProcess::handleError(const Error &error) { showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message } else if (id == u"BOT_PRECHECKOUT_FAILED"_q) { showToast({ "Error: PreCheckout Failed. Your card has not been billed." }); // #TODO payments errors message - } else if (id == u"INVOICE_ALREADY_PAID"_q) { - showToast({ "Already Paid!" }); // #TODO payments errors message } else if (id == u"REQUESTED_INFO_INVALID"_q || id == u"SHIPPING_OPTION_INVALID"_q || id == u"PAYMENT_CREDENTIALS_INVALID"_q @@ -448,7 +444,8 @@ void CheckoutProcess::panelShowBox(object_ptr box) { void CheckoutProcess::performInitialSilentValidation() { const auto &invoice = _form->invoice(); const auto &saved = _form->savedInformation(); - if ((invoice.isNameRequested && saved.name.isEmpty()) + if (invoice.receipt + || (invoice.isNameRequested && saved.name.isEmpty()) || (invoice.isEmailRequested && saved.email.isEmpty()) || (invoice.isPhoneRequested && saved.phone.isEmpty()) || (invoice.isShippingAddressRequested && !saved.shippingAddress)) { diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 3cc62685e..604bba091 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -94,7 +94,11 @@ Form::Form(not_null session, FullMsgId itemId) , _api(&_session->mtp()) , _msgId(itemId) { fillInvoiceFromMessage(); - requestForm(); + if (_receiptMsgId) { + requestReceipt(); + } else { + requestForm(); + } } Form::~Form() = default; @@ -103,10 +107,16 @@ void Form::fillInvoiceFromMessage() { if (const auto item = _session->data().message(_msgId)) { if (const auto media = item->media()) { if (const auto invoice = media->invoice()) { + _receiptMsgId = FullMsgId( + _msgId.channel, + invoice->receiptMsgId); _invoice.cover = Ui::Cover{ .title = invoice->title, .description = invoice->description, }; + if (_receiptMsgId) { + _invoice.receipt.paid = true; + } if (const auto photo = invoice->photo) { loadThumbnail(photo); } @@ -205,6 +215,18 @@ void Form::requestForm() { }).send(); } +void Form::requestReceipt() { + _api.request(MTPpayments_GetPaymentReceipt( + MTP_int(_receiptMsgId.msg) + )).done([=](const MTPpayments_PaymentReceipt &result) { + result.match([&](const auto &data) { + processReceipt(data); + }); + }).fail([=](const MTP::Error &error) { + _updates.fire(Error{ Error::Type::Form, error.type() }); + }).send(); +} + void Form::processForm(const MTPDpayments_paymentForm &data) { _session->data().processUsers(data.vusers()); @@ -226,6 +248,32 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { _updates.fire(FormReady{}); } +void Form::processReceipt(const MTPDpayments_paymentReceipt &data) { + _session->data().processUsers(data.vusers()); + + data.vinvoice().match([&](const auto &data) { + processInvoice(data); + }); + processDetails(data); + if (const auto info = data.vinfo()) { + info->match([&](const auto &data) { + processSavedInformation(data); + }); + } + if (const auto shipping = data.vshipping()) { + processShippingOptions({ *shipping }); + if (!_shippingOptions.list.empty()) { + _shippingOptions.selectedId = _shippingOptions.list.front().id; + } + } + _paymentMethod.savedCredentials = SavedCredentials{ + .id = "(used)", + .title = qs(data.vcredentials_title()), + }; + fillPaymentMethodInformation(); + _updates.fire(FormReady{}); +} + void Form::processInvoice(const MTPDinvoice &data) { _invoice = Ui::Invoice{ .cover = std::move(_invoice.cover), @@ -246,7 +294,6 @@ void Form::processInvoice(const MTPDinvoice &data) { } void Form::processDetails(const MTPDpayments_paymentForm &data) { - _session->data().processUsers(data.vusers()); const auto nativeParams = data.vnative_params(); auto nativeParamsJson = nativeParams ? nativeParams->match( @@ -268,6 +315,25 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { } } +void Form::processDetails(const MTPDpayments_paymentReceipt &data) { + _invoice.receipt = Ui::Receipt{ + .date = data.vdate().v, + .totalAmount = *reinterpret_cast( + &data.vtotal_amount().v), + .currency = qs(data.vcurrency()), + .paid = true, + }; + _details = FormDetails{ + .botId = data.vbot_id().v, + .providerId = data.vprovider_id().v, + }; + if (_details.botId) { + if (const auto bot = _session->data().userLoaded(_details.botId)) { + _invoice.cover.seller = bot->name; + } + } +} + void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); _savedInformation = Ui::RequestedInformation{ diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index c964a6683..d627df020 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -186,9 +186,12 @@ private: [[nodiscard]] QImage prepareEmptyThumbnail() const; void requestForm(); + void requestReceipt(); void processForm(const MTPDpayments_paymentForm &data); + void processReceipt(const MTPDpayments_paymentReceipt &data); void processInvoice(const MTPDinvoice &data); void processDetails(const MTPDpayments_paymentForm &data); + void processDetails(const MTPDpayments_paymentReceipt &data); void processSavedInformation(const MTPDpaymentRequestedInfo &data); void processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data); @@ -217,6 +220,7 @@ private: const not_null _session; MTP::Sender _api; FullMsgId _msgId; + FullMsgId _receiptMsgId; Ui::Invoice _invoice; std::unique_ptr _thumbnailLoadProcess; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index aae6bd1af..134ca52af 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "data/data_countries.h" #include "lang/lang_keys.h" +#include "base/unixtime.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -187,17 +188,14 @@ void FormSummary::setupCover(not_null layout) { void FormSummary::setupPrices(not_null layout) { Settings::AddSkip(layout, st::paymentsPricesTopSkip); - const auto add = [&]( + const auto addRow = [&]( const QString &label, - int64 amount, + const QString &value, bool full = false) { const auto &st = full ? st::paymentsFullPriceAmount : st::paymentsPriceAmount; - const auto right = CreateChild( - layout.get(), - formatAmount(amount), - st); + const auto right = CreateChild(layout.get(), value, st); const auto &padding = st::paymentsPricePadding; const auto left = layout->add( object_ptr( @@ -220,6 +218,12 @@ void FormSummary::setupPrices(not_null layout) { right->moveToRight(st::paymentsPricePadding.right(), top, width); }, right->lifetime()); }; + const auto add = [&]( + const QString &label, + int64 amount, + bool full = false) { + addRow(label, formatAmount(amount), full); + }; for (const auto &price : _invoice.prices) { add(price.label, price.price); } @@ -234,6 +238,15 @@ void FormSummary::setupPrices(not_null layout) { } add(tr::lng_payments_total_label(tr::now), computeTotalAmount(), true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + if (_invoice.receipt) { + Settings::AddDivider(layout); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + addRow( + tr::lng_payments_date_label(tr::now), + langDateTime(base::unixtime::parse(_invoice.receipt.date)), + true); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + } } void FormSummary::setupSections(not_null layout) { @@ -244,13 +257,16 @@ void FormSummary::setupSections(not_null layout) { const QString &label, const style::icon *icon, Fn handler) { - Settings::AddButtonWithLabel( + const auto button = Settings::AddButtonWithLabel( layout, std::move(title), rpl::single(label), st::paymentsSectionButton, - icon - )->addClickHandler(std::move(handler)); + icon); + button->addClickHandler(std::move(handler)); + if (_invoice.receipt) { + button->setAttribute(Qt::WA_TransparentForMouseEvents); + } }; add( tr::lng_payments_payment_method(), diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index d39a7ab69..f5c7015cc 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -51,7 +51,9 @@ void Panel::showForm( const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options) { - _widget->setTitle(tr::lng_payments_checkout_title()); + _widget->setTitle(invoice.receipt + ? tr::lng_payments_receipt_title() + : tr::lng_payments_checkout_title()); auto form = base::make_unique_q( _widget.get(), invoice, diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index da920af39..e530bf6b7 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -21,11 +21,26 @@ struct Cover { QImage thumbnail; }; +struct Receipt { + TimeId date = 0; + int64 totalAmount = 0; + QString currency; + bool paid = false; + + [[nodiscard]] bool empty() const { + return !paid; + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } +}; + struct Invoice { Cover cover; std::vector prices; QString currency; + Receipt receipt; bool isNameRequested = false; bool isPhoneRequested = false; From 3ec3f6484fa9b88e04cc76e7903cf47f9f91c5cb Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 12:16:05 +0400 Subject: [PATCH 026/127] Update API scheme to layer 128. --- Telegram/Resources/langs/lang.strings | 2 +- Telegram/Resources/tl/api.tl | 40 +++++------ .../SourceFiles/calls/calls_group_call.cpp | 6 +- Telegram/SourceFiles/data/data_channel.cpp | 6 +- Telegram/SourceFiles/data/data_channel.h | 4 +- Telegram/SourceFiles/data/data_chat.cpp | 6 +- Telegram/SourceFiles/data/data_chat.h | 4 +- Telegram/SourceFiles/data/data_peer.cpp | 17 ++--- Telegram/SourceFiles/data/data_peer.h | 5 +- Telegram/SourceFiles/data/data_user.cpp | 5 +- .../data/stickers/data_stickers.cpp | 1 + .../SourceFiles/export/export_api_wrap.cpp | 34 ++-------- Telegram/SourceFiles/facades.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 49 +++++--------- .../payments/payments_checkout_process.cpp | 32 +++++++-- .../payments/payments_checkout_process.h | 12 +++- .../SourceFiles/payments/payments_form.cpp | 66 ++++++++++++------- Telegram/SourceFiles/payments/payments_form.h | 10 ++- .../payments/ui/payments_form_summary.cpp | 22 ++++--- .../SourceFiles/storage/localimageloader.cpp | 6 +- .../SourceFiles/ui/image/image_location.cpp | 29 ++++---- .../ui/image/image_location_factory.cpp | 14 ++-- .../window/themes/window_theme.cpp | 1 - .../window/themes/window_theme_editor_box.cpp | 1 - 24 files changed, 172 insertions(+), 202 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1366dd31d..2b6c73d04 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1863,7 +1863,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_checkout_title" = "Checkout"; "lng_payments_receipt_title" = "Receipt"; "lng_payments_total_label" = "Total"; -"lng_payments_date_label" = "Date"; +"lng_payments_date_label" = "Paid"; "lng_payments_pay_amount" = "Pay {amount}"; "lng_payments_payment_method" = "Payment Method"; "lng_payments_new_card" = "New Card..."; diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 22bc8cf22..6124140f9 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -90,8 +90,8 @@ inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation; inputTakeoutFileLocation#29be5899 = InputFileLocation; inputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; inputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation; -inputPeerPhotoFileLocation#27d69997 flags:# big:flags.0?true peer:InputPeer volume_id:long local_id:int = InputFileLocation; -inputStickerSetThumb#dbaeae9 stickerset:InputStickerSet volume_id:long local_id:int = InputFileLocation; +inputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation; +inputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation; inputGroupCallStream#bba51639 call:InputGroupCall time_ms:long scale:int = InputFileLocation; peerUser#9db1bc6d user_id:int = Peer; @@ -113,7 +113,7 @@ userEmpty#200250ba id:int = User; user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; -userProfilePhoto#69d3ab26 flags:# has_video:flags.0?true photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto; +userProfilePhoto#82d1f706 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; userStatusEmpty#9d05049 = UserStatus; userStatusOnline#edb93949 expires:int = UserStatus; @@ -139,7 +139,7 @@ chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0? chatParticipants#3f460fed chat_id:int participants:Vector version:int = ChatParticipants; chatPhotoEmpty#37c1011c = ChatPhoto; -chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto; +chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; message#bce383d2 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; @@ -194,10 +194,10 @@ photoEmpty#2331b22d id:long = Photo; photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector video_sizes:flags.1?Vector dc_id:int = Photo; photoSizeEmpty#e17e23c type:string = PhotoSize; -photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = PhotoSize; -photoCachedSize#e9a734fa type:string location:FileLocation w:int h:int bytes:bytes = PhotoSize; +photoSize#75c78e60 type:string w:int h:int size:int = PhotoSize; +photoCachedSize#21e1ad6 type:string w:int h:int bytes:bytes = PhotoSize; photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; -photoSizeProgressive#5aa86a51 type:string location:FileLocation w:int h:int sizes:Vector = PhotoSize; +photoSizeProgressive#fa3efb95 type:string w:int h:int sizes:Vector = PhotoSize; photoPathSize#d8214d41 type:string bytes:bytes = PhotoSize; geoPointEmpty#1117dd5f = GeoPoint; @@ -554,7 +554,7 @@ inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; -stickerSet#40e237a8 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int count:int hash:int = StickerSet; +stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; @@ -648,6 +648,7 @@ inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:f inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaInvoice#d5348d85 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; @@ -659,6 +660,7 @@ botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string ent botInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult; botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; @@ -792,7 +794,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector = Invoice; +invoice#24b6f6cd flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true multiple_allowed:flags.9?true can_forward:flags.10?true currency:string prices:Vector min_tip_amount:flags.8?long max_tip_amount:flags.8?long default_tip_amount:flags.8?long = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -812,14 +814,14 @@ inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; -payments.paymentForm#3f56aea3 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true bot_id:int invoice:Invoice provider_id:int url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; +payments.paymentForm#8d0b2415 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:int invoice:Invoice provider_id:int url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; -payments.paymentReceipt#500911e1 flags:# date:int bot_id:int invoice:Invoice provider_id:int info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; +payments.paymentReceipt#10b555d0 flags:# date:int bot_id:int provider_id:int title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; @@ -1088,8 +1090,6 @@ emojiURL#a575739d url:string = EmojiURL; emojiLanguage#b3fb5361 lang_code:string = EmojiLanguage; -fileLocationToBeDeprecated#bc7fc6cd volume_id:long local_id:int = FileLocation; - folder#ff544e65 flags:# autofill_new_broadcasts:flags.0?true autofill_public_groups:flags.1?true autofill_new_correspondents:flags.2?true id:int title:string photo:flags.3?ChatPhoto = Folder; inputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer; @@ -1169,7 +1169,7 @@ stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueA help.promoDataEmpty#98f6ac75 expires:int = help.PromoData; help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector users:Vector psa_type:flags.1?string psa_message:flags.2?string = help.PromoData; -videoSize#e831c556 flags:# type:string location:FileLocation w:int h:int size:int video_start_ts:flags.0?double = VideoSize; +videoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize; statsGroupTopPoster#18f3d0f7 user_id:int messages:int avg_chars:int = StatsGroupTopPoster; @@ -1207,7 +1207,7 @@ groupCall#c0c2052e flags:# join_muted:flags.1?true can_change_join_muted:flags.2 inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; -groupCallParticipant#19adba89 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long = GroupCallParticipant; +groupCallParticipant#b96b25ee flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long params:flags.6?DataJSON = GroupCallParticipant; phone.groupCall#9e727aad call:GroupCall participants:Vector participants_next_offset:string chats:Vector users:Vector = phone.GroupCall; @@ -1592,10 +1592,10 @@ bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; bots.setBotCommands#805d46f6 commands:Vector = Bool; -payments.getPaymentForm#99f09745 msg_id:int = payments.PaymentForm; -payments.getPaymentReceipt#a092a980 msg_id:int = payments.PaymentReceipt; -payments.validateRequestedInfo#770a8e74 flags:# save:flags.0?true msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; -payments.sendPaymentForm#2b8879b3 flags:# msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials = payments.PaymentResult; +payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm; +payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; +payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; +payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult; payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; @@ -1645,4 +1645,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 126 +// LAYER 128 diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index 176d68f6a..1faa1336a 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -561,7 +561,8 @@ void GroupCall::applyMeInCallLocally() { MTP_int(_mySsrc), MTP_int(volume), MTPstring(), // Don't update about text in local updates. - MTP_long(raisedHandRating))), + MTP_long(raisedHandRating), + MTPDataJSON())), MTP_int(0)).c_updateGroupCallParticipants()); } @@ -606,7 +607,8 @@ void GroupCall::applyParticipantLocally( MTP_int(participant->ssrc), MTP_int(volume.value_or(participant->volume)), MTPstring(), // Don't update about text in local updates. - MTP_long(participant->raisedHandRating))), + MTP_long(participant->raisedHandRating), + MTPDataJSON())), MTP_int(0)).c_updateGroupCallParticipants()); } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index b70b0fa61..6ff73eb6e 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -77,12 +77,8 @@ ChannelData::ChannelData(not_null owner, PeerId id) } void ChannelData::setPhoto(const MTPChatPhoto &photo) { - setPhoto(userpicPhotoId(), photo); -} - -void ChannelData::setPhoto(PhotoId photoId, const MTPChatPhoto &photo) { photo.match([&](const MTPDchatPhoto & data) { - updateUserpic(photoId, data.vdc_id().v, data.vphoto_small()); + updateUserpic(data.vphoto_id().v, data.vdc_id().v); }, [&](const MTPDchatPhotoEmpty &) { clearUserpic(); }); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index e00d22a87..5047e223d 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -126,10 +126,8 @@ public: ChannelData(not_null owner, PeerId id); - void setPhoto(const MTPChatPhoto &photo); - void setPhoto(PhotoId photoId, const MTPChatPhoto &photo); - void setName(const QString &name, const QString &username); + void setPhoto(const MTPChatPhoto &photo); void setAccessHash(uint64 accessHash); void setFlags(MTPDchannel::Flags which) { diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index c12fdb867..44ba44d1d 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -37,12 +37,8 @@ ChatData::ChatData(not_null owner, PeerId id) } void ChatData::setPhoto(const MTPChatPhoto &photo) { - setPhoto(userpicPhotoId(), photo); -} - -void ChatData::setPhoto(PhotoId photoId, const MTPChatPhoto &photo) { photo.match([&](const MTPDchatPhoto &data) { - updateUserpic(photoId, data.vdc_id().v, data.vphoto_small()); + updateUserpic(data.vphoto_id().v, data.vdc_id().v); }, [&](const MTPDchatPhotoEmpty &) { clearUserpic(); }); diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index 595a50542..35ba2b089 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -39,10 +39,8 @@ public: ChatData(not_null owner, PeerId id); - void setPhoto(const MTPChatPhoto &photo); - void setPhoto(PhotoId photoId, const MTPChatPhoto &photo); - void setName(const QString &newName); + void setPhoto(const MTPChatPhoto &photo); void invalidateParticipants(); [[nodiscard]] bool noParticipantInfo() const { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 8648943de..374d06264 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -377,24 +377,19 @@ Data::FileOrigin PeerData::userpicPhotoOrigin() const { : Data::FileOrigin(); } -void PeerData::updateUserpic( - PhotoId photoId, - MTP::DcId dcId, - const MTPFileLocation &location) { - setUserpicChecked(photoId, location.match([&]( - const MTPDfileLocationToBeDeprecated &deprecated) { - return ImageLocation( +void PeerData::updateUserpic(PhotoId photoId, MTP::DcId dcId) { + setUserpicChecked( + photoId, + ImageLocation( { StorageFileLocation( dcId, isSelf() ? peerToUser(id) : 0, MTP_inputPeerPhotoFileLocation( MTP_flags(0), input, - deprecated.vvolume_id(), - deprecated.vlocal_id())) }, + MTP_long(photoId))) }, kUserpicSize, - kUserpicSize); - })); + kUserpicSize)); } void PeerData::clearUserpic() { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 960f0751a..fff3a65d1 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -404,10 +404,7 @@ protected: const QString &newName, const QString &newNameOrPhone, const QString &newUsername); - void updateUserpic( - PhotoId photoId, - MTP::DcId dcId, - const MTPFileLocation &small); + void updateUserpic(PhotoId photoId, MTP::DcId dcId); void clearUserpic(); private: diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 03fb7530e..a555781a9 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -72,10 +72,7 @@ void UserData::setIsContact(bool is) { // see Serialize::readPeer as well void UserData::setPhoto(const MTPUserProfilePhoto &photo) { photo.match([&](const MTPDuserProfilePhoto &data) { - updateUserpic( - data.vphoto_id().v, - data.vdc_id().v, - data.vphoto_small()); + updateUserpic(data.vphoto_id().v, data.vdc_id().v); }, [&](const MTPDuserProfilePhotoEmpty &) { clearUserpic(); }); diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index d2a062206..f5212ca11 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -324,6 +324,7 @@ bool Stickers::applyArchivedResultFake() { MTP_string(raw->shortName), MTP_vector(), MTP_int(0), + MTP_int(0), MTP_int(raw->count), MTP_int(raw->hash)); sets.push_back(MTP_stickerSetCovered( diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 1a89a9568..997773ed0 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -49,25 +49,13 @@ std::tuple value_ordering_helper(const LocationK LocationKey ComputeLocationKey(const Data::FileLocation &value) { auto result = LocationKey(); result.type = value.dcId; - value.data.match([&](const MTPDinputFileLocation &data) { - result.type |= (1ULL << 24); - result.type |= (uint64(uint32(data.vlocal_id().v)) << 32); - result.id = data.vvolume_id().v; - }, [&](const MTPDinputDocumentFileLocation &data) { + value.data.match([&](const MTPDinputDocumentFileLocation &data) { const auto letter = data.vthumb_size().v.isEmpty() ? char(0) : data.vthumb_size().v[0]; result.type |= (2ULL << 24); result.type |= (uint64(uint32(letter)) << 16); result.id = data.vid().v; - }, [&](const MTPDinputSecureFileLocation &data) { - result.type |= (3ULL << 24); - result.id = data.vid().v; - }, [&](const MTPDinputEncryptedFileLocation &data) { - result.type |= (4ULL << 24); - result.id = data.vid().v; - }, [&](const MTPDinputTakeoutFileLocation &data) { - result.type |= (5ULL << 24); }, [&](const MTPDinputPhotoFileLocation &data) { const auto letter = data.vthumb_size().v.isEmpty() ? char(0) @@ -75,22 +63,10 @@ LocationKey ComputeLocationKey(const Data::FileLocation &value) { result.type |= (6ULL << 24); result.type |= (uint64(uint32(letter)) << 16); result.id = data.vid().v; - }, [&](const MTPDinputPeerPhotoFileLocation &data) { - const auto letter = data.is_big() ? char(1) : char(0); - result.type |= (7ULL << 24); - result.type |= (uint64(uint32(data.vlocal_id().v)) << 32); - result.type |= (uint64(uint32(letter)) << 16); - result.id = data.vvolume_id().v; - }, [&](const MTPDinputStickerSetThumb &data) { - result.type |= (8ULL << 24); - result.type |= (uint64(uint32(data.vlocal_id().v)) << 32); - result.id = data.vvolume_id().v; - }, [&](const MTPDinputPhotoLegacyFileLocation &data) { - result.type |= (9ULL << 24); - result.type |= (uint64(uint32(data.vlocal_id().v)) << 32); - result.id = data.vvolume_id().v; - }, [&](const MTPDinputGroupCallStream &data) { - result.type = (10ULL << 24); + }, [&](const MTPDinputTakeoutFileLocation &data) { + result.type |= (5ULL << 24); + }, [](const auto &data) { + Unexpected("File location type in Export::ComputeLocationKey."); }); return result; } diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 31080b485..023f2da4f 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -122,7 +122,7 @@ void activateBotCommand( } break; case ButtonType::Buy: { - Payments::CheckoutProcess::Start(msg); + Payments::CheckoutProcess::Start(msg, Payments::Mode::Payment); } break; case ButtonType::Url: { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index c678d24d8..4415d1af3 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -954,40 +954,25 @@ void History::applyServiceChanges( } break; case mtpc_messageActionChatEditPhoto: { - auto &d = action.c_messageActionChatEditPhoto(); + const auto &d = action.c_messageActionChatEditPhoto(); d.vphoto().match([&](const MTPDphoto &data) { - const auto &sizes = data.vsizes().v; - if (!sizes.isEmpty()) { - auto photo = owner().processPhoto(data); - photo->peer = peer; - auto &smallSize = sizes.front(); - auto &bigSize = sizes.back(); - const MTPFileLocation *smallLoc = nullptr; - const MTPFileLocation *bigLoc = nullptr; - switch (smallSize.type()) { - case mtpc_photoSize: smallLoc = &smallSize.c_photoSize().vlocation(); break; - case mtpc_photoCachedSize: smallLoc = &smallSize.c_photoCachedSize().vlocation(); break; - } - switch (bigSize.type()) { - case mtpc_photoSize: bigLoc = &bigSize.c_photoSize().vlocation(); break; - case mtpc_photoCachedSize: bigLoc = &bigSize.c_photoCachedSize().vlocation(); break; - } - if (smallLoc && bigLoc) { - const auto chatPhoto = MTP_chatPhoto( - MTP_flags(photo->hasVideo() - ? MTPDchatPhoto::Flag::f_has_video - : MTPDchatPhoto::Flag(0)), - *smallLoc, - *bigLoc, - data.vdc_id()); - if (const auto chat = peer->asChat()) { - chat->setPhoto(photo->id, chatPhoto); - } else if (const auto channel = peer->asChannel()) { - channel->setPhoto(photo->id, chatPhoto); - } - peer->loadUserpic(); - } + using Flag = MTPDchatPhoto::Flag; + const auto photo = owner().processPhoto(data); + photo->peer = peer; + const auto chatPhoto = MTP_chatPhoto( + MTP_flags((photo->hasVideo() ? Flag::f_has_video : Flag(0)) + | (photo->inlineThumbnailBytes().isEmpty() + ? Flag(0) + : Flag::f_stripped_thumb)), + MTP_long(photo->id), + MTP_bytes(photo->inlineThumbnailBytes()), + data.vdc_id()); + if (const auto chat = peer->asChat()) { + chat->setPhoto(chatPhoto); + } else if (const auto channel = peer->asChannel()) { + channel->setPhoto(chatPhoto); } + peer->loadUserpic(); }, [&](const MTPDphotoEmpty &data) { if (const auto chat = peer->asChat()) { chat->setPhoto(MTP_chatPhotoEmpty()); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index f5508fb7f..42f287afb 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -57,10 +57,23 @@ base::flat_map, SessionProcesses> Processes; } // namespace -void CheckoutProcess::Start(not_null item) { +void CheckoutProcess::Start(not_null item, Mode mode) { auto &processes = LookupSessionProcesses(item); const auto session = &item->history()->session(); - const auto id = item->fullId(); + const auto media = item->media(); + const auto invoice = media ? media->invoice() : nullptr; + if (mode == Mode::Payment && !invoice) { + return; + } + const auto id = (invoice && invoice->receiptMsgId) + ? FullMsgId(item->history()->channelId(), invoice->receiptMsgId) + : item->fullId(); + if (invoice) { + mode = invoice->receiptMsgId ? Mode::Receipt : Mode::Payment; + } else if (mode == Mode::Payment) { + LOG(("API Error: CheckoutProcess Payment start without invoice.")); + return; + } const auto i = processes.map.find(id); if (i != end(processes.map)) { i->second->requestActivate(); @@ -68,16 +81,21 @@ void CheckoutProcess::Start(not_null item) { } const auto j = processes.map.emplace( id, - std::make_unique(session, id, PrivateTag{})).first; + std::make_unique( + item->history()->peer, + id.msg, + mode, + PrivateTag{})).first; j->second->requestActivate(); } CheckoutProcess::CheckoutProcess( - not_null session, - FullMsgId itemId, + not_null peer, + MsgId itemId, + Mode mode, PrivateTag) -: _session(session) -, _form(std::make_unique(session, itemId)) +: _session(&peer->session()) +, _form(std::make_unique(peer, itemId, (mode == Mode::Receipt))) , _panel(std::make_unique(panelDelegate())) { _form->updates( ) | rpl::start_with_next([=](const FormUpdate &update) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index de9d37724..794acdcec 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -28,17 +28,23 @@ class Form; struct FormUpdate; struct Error; +enum class Mode { + Payment, + Receipt, +}; + class CheckoutProcess final : public base::has_weak_ptr , private Ui::PanelDelegate { struct PrivateTag {}; public: - static void Start(not_null item); + static void Start(not_null item, Mode mode); CheckoutProcess( - not_null session, - FullMsgId itemId, + not_null peer, + MsgId itemId, + Mode mode, PrivateTag); ~CheckoutProcess(); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 604bba091..7e183d625 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_countries.h" #include "history/history_item.h" +#include "history/history_service.h" // HistoryServicePayment. #include "stripe/stripe_api_client.h" #include "stripe/stripe_error.h" #include "stripe/stripe_token.h" @@ -89,12 +90,15 @@ namespace { } // namespace -Form::Form(not_null session, FullMsgId itemId) -: _session(session) +Form::Form(not_null peer, MsgId itemId, bool receipt) +: _session(&peer->session()) , _api(&_session->mtp()) -, _msgId(itemId) { +, _peer(peer) +, _msgId(itemId) +, _receiptMode(receipt) { fillInvoiceFromMessage(); - if (_receiptMsgId) { + if (_receiptMode) { + _invoice.receipt.paid = true; requestReceipt(); } else { requestForm(); @@ -104,23 +108,24 @@ Form::Form(not_null session, FullMsgId itemId) Form::~Form() = default; void Form::fillInvoiceFromMessage() { - if (const auto item = _session->data().message(_msgId)) { - if (const auto media = item->media()) { - if (const auto invoice = media->invoice()) { - _receiptMsgId = FullMsgId( - _msgId.channel, - invoice->receiptMsgId); - _invoice.cover = Ui::Cover{ - .title = invoice->title, - .description = invoice->description, - }; - if (_receiptMsgId) { - _invoice.receipt.paid = true; - } - if (const auto photo = invoice->photo) { - loadThumbnail(photo); + const auto id = FullMsgId(peerToChannel(_peer->id), _msgId); + if (const auto item = _session->data().message(id)) { + const auto media = [&] { + if (const auto payment = item->Get()) { + if (payment->msg) { + return payment->msg->media(); } } + return item->media(); + }(); + if (const auto invoice = media ? media->invoice() : nullptr) { + _invoice.cover = Ui::Cover{ + .title = invoice->title, + .description = invoice->description, + }; + if (const auto photo = invoice->photo) { + loadThumbnail(photo); + } } } } @@ -141,7 +146,9 @@ void Form::loadThumbnail(not_null photo) { _invoice.cover.thumbnail = prepareEmptyThumbnail(); } _thumbnailLoadProcess->view = std::move(view); - photo->load(Data::PhotoSize::Thumbnail, _msgId); + photo->load( + Data::PhotoSize::Thumbnail, + FullMsgId(peerToChannel(_peer->id), _msgId)); _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { const auto &view = _thumbnailLoadProcess->view; @@ -205,7 +212,10 @@ QImage Form::prepareEmptyThumbnail() const { void Form::requestForm() { _api.request(MTPpayments_GetPaymentForm( - MTP_int(_msgId.msg) + MTP_flags(0), + _peer->input, + MTP_int(_msgId), + MTP_dataJSON(MTP_string(QString())) )).done([=](const MTPpayments_PaymentForm &result) { result.match([&](const auto &data) { processForm(data); @@ -217,7 +227,8 @@ void Form::requestForm() { void Form::requestReceipt() { _api.request(MTPpayments_GetPaymentReceipt( - MTP_int(_receiptMsgId.msg) + _peer->input, + MTP_int(_msgId) )).done([=](const MTPpayments_PaymentReceipt &result) { result.match([&](const auto &data) { processReceipt(data); @@ -300,6 +311,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { [&](const MTPDdataJSON &data) { return data.vdata().v; }) : QByteArray(); _details = FormDetails{ + .formId = data.vform_id().v, .url = qs(data.vurl()), .nativeProvider = qs(data.vnative_provider().value_or_empty()), .nativeParamsJson = std::move(nativeParamsJson), @@ -429,12 +441,15 @@ void Form::submit() { | (_shippingOptions.selectedId.isEmpty() ? Flag(0) : Flag::f_shipping_option_id)), - MTP_int(_msgId.msg), + MTP_long(_details.formId), + _peer->input, + MTP_int(_msgId), MTP_string(_requestedInformationId), MTP_string(_shippingOptions.selectedId), MTP_inputPaymentCredentials( MTP_flags(0), - MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))) + MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))), + MTP_long(0) // #TODO payments tip_amount )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { _updates.fire(PaymentFinished{ data.vupdates() }); @@ -466,7 +481,8 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( MTP_flags(0), // #TODO payments save information - MTP_int(_msgId.msg), + _peer->input, + MTP_int(_msgId), Serialize(information) )).done([=](const MTPpayments_ValidatedRequestedInfo &result) { _validateRequestId = 0; diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index d627df020..ca753eb31 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -27,7 +27,10 @@ class PhotoMedia; namespace Payments { +enum class Mode; + struct FormDetails { + uint64 formId = 0; QString url; QString nativeProvider; QByteArray nativeParamsJson; @@ -143,7 +146,7 @@ struct FormUpdate : std::variant< class Form final : public base::has_weak_ptr { public: - Form(not_null session, FullMsgId itemId); + Form(not_null peer, MsgId itemId, bool receipt); ~Form(); [[nodiscard]] const Ui::Invoice &invoice() const { @@ -219,8 +222,9 @@ private: const not_null _session; MTP::Sender _api; - FullMsgId _msgId; - FullMsgId _receiptMsgId; + not_null _peer; + MsgId _msgId = 0; + bool _receiptMode = false; Ui::Invoice _invoice; std::unique_ptr _thumbnailLoadProcess; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 134ca52af..95d647ec9 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -187,7 +187,6 @@ void FormSummary::setupCover(not_null layout) { } void FormSummary::setupPrices(not_null layout) { - Settings::AddSkip(layout, st::paymentsPricesTopSkip); const auto addRow = [&]( const QString &label, const QString &value, @@ -218,6 +217,18 @@ void FormSummary::setupPrices(not_null layout) { right->moveToRight(st::paymentsPricePadding.right(), top, width); }, right->lifetime()); }; + + Settings::AddSkip(layout, st::paymentsPricesTopSkip); + if (_invoice.receipt) { + Settings::AddDivider(layout); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + addRow( + tr::lng_payments_date_label(tr::now), + langDateTime(base::unixtime::parse(_invoice.receipt.date)), + true); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + } + const auto add = [&]( const QString &label, int64 amount, @@ -238,15 +249,6 @@ void FormSummary::setupPrices(not_null layout) { } add(tr::lng_payments_total_label(tr::now), computeTotalAmount(), true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); - if (_invoice.receipt) { - Settings::AddDivider(layout); - Settings::AddSkip(layout, st::paymentsPricesBottomSkip); - addRow( - tr::lng_payments_date_label(tr::now), - langDateTime(base::unixtime::parse(_invoice.receipt.date)), - true); - Settings::AddSkip(layout, st::paymentsPricesBottomSkip); - } } void FormSummary::setupSections(not_null layout) { diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 8a88e656b..d2a171bfb 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -79,7 +79,6 @@ PreparedFileThumbnail PrepareFileThumbnail(QImage &&original) { : std::move(original); result.mtpSize = MTP_photoSize( MTP_string(), - MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(result.image.width()), MTP_int(result.image.height()), MTP_int(0)); @@ -210,7 +209,6 @@ SendMediaReady PreparePeerPhoto(MTP::DcId dcId, PeerId peerId, QImage &&image) { QByteArray bytes = QByteArray()) { photoSizes.push_back(MTP_photoSize( MTP_string(type), - MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(image.width()), MTP_int(image.height()), MTP_int(0))); photoThumbs.emplace(type[0], PreparedPhotoThumb{ @@ -887,13 +885,13 @@ void FileLoadTask::process(Args &&args) { writer.write(full); } photoThumbs.emplace('m', PreparedPhotoThumb{ .image = medium }); - photoSizes.push_back(MTP_photoSize(MTP_string("m"), MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0))); + photoSizes.push_back(MTP_photoSize(MTP_string("m"), MTP_int(medium.width()), MTP_int(medium.height()), MTP_int(0))); photoThumbs.emplace('y', PreparedPhotoThumb{ .image = full, .bytes = filedata }); - photoSizes.push_back(MTP_photoSize(MTP_string("y"), MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0))); + photoSizes.push_back(MTP_photoSize(MTP_string("y"), MTP_int(full.width()), MTP_int(full.height()), MTP_int(0))); photo = MTP_photo( MTP_flags(0), diff --git a/Telegram/SourceFiles/ui/image/image_location.cpp b/Telegram/SourceFiles/ui/image/image_location.cpp index 608a89b8c..c3f72b63f 100644 --- a/Telegram/SourceFiles/ui/image/image_location.cpp +++ b/Telegram/SourceFiles/ui/image/image_location.cpp @@ -119,14 +119,14 @@ StorageFileLocation::StorageFileLocation( const auto fillPeer = base::overload([&]( const MTPDinputPeerEmpty &data) { _id = 0; - }, [&](const MTPDinputPeerSelf & data) { + }, [&](const MTPDinputPeerSelf &data) { _id = peerFromUser(self); - }, [&](const MTPDinputPeerChat & data) { + }, [&](const MTPDinputPeerChat &data) { _id = peerFromChat(data.vchat_id()); - }, [&](const MTPDinputPeerUser & data) { + }, [&](const MTPDinputPeerUser &data) { _id = peerFromUser(data.vuser_id()); _accessHash = data.vaccess_hash().v; - }, [&](const MTPDinputPeerChannel & data) { + }, [&](const MTPDinputPeerChannel &data) { _id = peerFromChannel(data.vchannel_id()); _accessHash = data.vaccess_hash().v; }); @@ -146,8 +146,8 @@ StorageFileLocation::StorageFileLocation( _inMessagePeerId = -data.vchannel_id().v; _inMessageId = data.vmsg_id().v; }); - _volumeId = data.vvolume_id().v; - _localId = data.vlocal_id().v; + _volumeId = data.vphoto_id().v; + _localId = 0; _sizeLetter = data.is_big() ? 'c' : 'a'; }, [&](const MTPDinputStickerSetThumb &data) { _type = Type::StickerSetThumb; @@ -156,16 +156,11 @@ StorageFileLocation::StorageFileLocation( }, [&](const MTPDinputStickerSetID &data) { _id = data.vid().v; _accessHash = data.vaccess_hash().v; - }, [&](const MTPDinputStickerSetShortName &data) { - Unexpected("inputStickerSetShortName in StorageFileLocation."); - }, [&](const MTPDinputStickerSetAnimatedEmoji &data) { - Unexpected( - "inputStickerSetAnimatedEmoji in StorageFileLocation."); - }, [&](const MTPDinputStickerSetDice &data) { - Unexpected("inputStickerSetDice in StorageFileLocation."); + }, [&](const auto &data) { + Unexpected("InputStickerSet type in StorageFileLocation."); }); - _volumeId = data.vvolume_id().v; - _localId = data.vlocal_id().v; + _volumeId = 0; + _localId = data.vthumb_version().v; }, [&](const MTPDinputGroupCallStream &data) { _type = Type::GroupCallStream; data.vcall().match([&](const MTPDinputGroupCall &data) { @@ -249,13 +244,11 @@ MTPInputFileLocation StorageFileLocation::tl(int32 self) const { _inMessagePeerId, _inMessageId, self), - MTP_long(_volumeId), - MTP_int(_localId)); + MTP_long(_volumeId)); case Type::StickerSetThumb: return MTP_inputStickerSetThumb( MTP_inputStickerSetID(MTP_long(_id), MTP_long(_accessHash)), - MTP_long(_volumeId), MTP_int(_localId)); case Type::GroupCallStream: diff --git a/Telegram/SourceFiles/ui/image/image_location_factory.cpp b/Telegram/SourceFiles/ui/image/image_location_factory.cpp index eefbc7157..57db9035c 100644 --- a/Telegram/SourceFiles/ui/image/image_location_factory.cpp +++ b/Telegram/SourceFiles/ui/image/image_location_factory.cpp @@ -196,11 +196,10 @@ ImageWithLocation FromPhotoSize( not_null session, const MTPDstickerSet &set, const MTPPhotoSize &size) { - if (!set.vthumb_dc_id()) { + if (!set.vthumb_dc_id() || !set.vthumb_version()) { return ImageWithLocation(); } return size.match([&](const MTPDphotoSize &data) { - const auto &location = data.vlocation().c_fileLocationToBeDeprecated(); return ImageWithLocation{ .location = ImageLocation( DownloadLocation{ StorageFileLocation( @@ -208,14 +207,12 @@ ImageWithLocation FromPhotoSize( session->userId(), MTP_inputStickerSetThumb( MTP_inputStickerSetID(set.vid(), set.vaccess_hash()), - location.vvolume_id(), - location.vlocal_id())) }, + MTP_int(set.vthumb_version()->v))) }, data.vw().v, data.vh().v), .bytesCount = data.vsize().v }; }, [&](const MTPDphotoCachedSize &data) { - const auto &location = data.vlocation().c_fileLocationToBeDeprecated(); const auto bytes = qba(data.vbytes()); return ImageWithLocation{ .location = ImageLocation( @@ -224,8 +221,7 @@ ImageWithLocation FromPhotoSize( session->userId(), MTP_inputStickerSetThumb( MTP_inputStickerSetID(set.vid(), set.vaccess_hash()), - location.vvolume_id(), - location.vlocal_id())) }, + MTP_int(set.vthumb_version()->v))) }, data.vw().v, data.vh().v), .bytes = bytes, @@ -235,7 +231,6 @@ ImageWithLocation FromPhotoSize( if (data.vsizes().v.isEmpty()) { return ImageWithLocation(); } - const auto &location = data.vlocation().c_fileLocationToBeDeprecated(); return ImageWithLocation{ .location = ImageLocation( DownloadLocation{ StorageFileLocation( @@ -243,8 +238,7 @@ ImageWithLocation FromPhotoSize( session->userId(), MTP_inputStickerSetThumb( MTP_inputStickerSetID(set.vid(), set.vaccess_hash()), - location.vvolume_id(), - location.vlocal_id())) }, + MTP_int(set.vthumb_version()->v))) }, data.vw().v, data.vh().v), .bytesCount = data.vsizes().v.back().v diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 94ca16a35..2aedf7902 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -461,7 +461,6 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { const auto push = [&](const char *type, QImage &&image) { sizes.push_back(MTP_photoSize( MTP_string(type), - MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(image.width()), MTP_int(image.height()), MTP_int(0))); thumbnails.emplace( diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp index 8bbeecd64..be67d6de3 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp @@ -433,7 +433,6 @@ SendMediaReady PrepareThemeMedia( QByteArray bytes = QByteArray()) { sizes.push_back(MTP_photoSize( MTP_string(type), - MTP_fileLocationToBeDeprecated(MTP_long(0), MTP_int(0)), MTP_int(image.width()), MTP_int(image.height()), MTP_int(0))); thumbnails.emplace(type[0], PreparedPhotoThumb{ From 888932941502cd9ea793cdd4234ab1d659d9d661 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 12:36:09 +0400 Subject: [PATCH 027/127] Support sending live location in inline bot results. --- .../inline_bots/inline_bot_result.cpp | 157 ++++++++---------- .../inline_bots/inline_bot_send_data.cpp | 13 +- .../inline_bots/inline_bot_send_data.h | 15 ++ 3 files changed, 100 insertions(+), 85 deletions(-) diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index e8e2e75e7..22bcb7511 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -149,10 +149,9 @@ std::unique_ptr Result::Create( message = &r.vsend_message(); } break; } - auto badAttachment = (result->_photo && result->_photo->isNull()) - || (result->_document && result->_document->isNull()); - - if (!message) { + if ((result->_photo && result->_photo->isNull()) + || (result->_document && result->_document->isNull()) + || !message) { return nullptr; } @@ -170,108 +169,98 @@ std::unique_ptr Result::Create( } } - switch (message->type()) { - case mtpc_botInlineMessageMediaAuto: { - const auto &r = message->c_botInlineMessageMediaAuto(); - const auto message = qs(r.vmessage()); + message->match([&](const MTPDbotInlineMessageMediaAuto &data) { + const auto message = qs(data.vmessage()); const auto entities = Api::EntitiesFromMTP( session, - r.ventities().value_or_empty()); + data.ventities().value_or_empty()); if (result->_type == Type::Photo) { - if (!result->_photo) { - return nullptr; + if (result->_photo) { + result->sendData = std::make_unique( + session, + result->_photo, + message, + entities); + } else { + LOG(("Inline Error: No 'photo' in media-auto, type=photo.")); } - result->sendData = std::make_unique( - session, - result->_photo, - message, - entities); } else if (result->_type == Type::Game) { result->createGame(session); result->sendData = std::make_unique( session, result->_game); } else { - if (!result->_document) { - return nullptr; + if (result->_document) { + result->sendData = std::make_unique( + session, + result->_document, + message, + entities); + } else { + LOG(("Inline Error: No 'document' in media-auto, type=%1." + ).arg(int(result->_type))); } - result->sendData = std::make_unique( - session, - result->_document, - message, - entities); } - if (const auto markup = r.vreply_markup()) { - result->_mtpKeyboard = std::make_unique(*markup); - } - } break; - - case mtpc_botInlineMessageText: { - const auto &r = message->c_botInlineMessageText(); + }, [&](const MTPDbotInlineMessageText &data) { result->sendData = std::make_unique( session, - qs(r.vmessage()), - Api::EntitiesFromMTP(session, r.ventities().value_or_empty()), - r.is_no_webpage()); - if (const auto markup = r.vreply_markup()) { - result->_mtpKeyboard = std::make_unique(*markup); - } - } break; - - case mtpc_botInlineMessageMediaGeo: { - // #TODO layer 72 save period and send live location?.. - auto &r = message->c_botInlineMessageMediaGeo(); - if (r.vgeo().type() == mtpc_geoPoint) { - result->sendData = std::make_unique( - session, - r.vgeo().c_geoPoint()); - } else { - badAttachment = true; - } - if (const auto markup = r.vreply_markup()) { - result->_mtpKeyboard = std::make_unique(*markup); - } - } break; - - case mtpc_botInlineMessageMediaVenue: { - auto &r = message->c_botInlineMessageMediaVenue(); - if (r.vgeo().type() == mtpc_geoPoint) { + qs(data.vmessage()), + Api::EntitiesFromMTP(session, data.ventities().value_or_empty()), + data.is_no_webpage()); + }, [&](const MTPDbotInlineMessageMediaGeo &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { + if (const auto period = data.vperiod()) { + result->sendData = std::make_unique( + session, + geo, + period->v, + (data.vheading() + ? std::make_optional(data.vheading()->v) + : std::nullopt), + (data.vproximity_notification_radius() + ? std::make_optional( + data.vproximity_notification_radius()->v) + : std::nullopt)); + } else { + result->sendData = std::make_unique( + session, + geo); + } + }, [&](const MTPDgeoPointEmpty &) { + LOG(("Inline Error: Empty 'geo' in media-geo.")); + }); + }, [&](const MTPDbotInlineMessageMediaVenue &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { result->sendData = std::make_unique( session, - r.vgeo().c_geoPoint(), - qs(r.vvenue_id()), - qs(r.vprovider()), - qs(r.vtitle()), - qs(r.vaddress())); - } else { - badAttachment = true; - } - if (const auto markup = r.vreply_markup()) { - result->_mtpKeyboard = std::make_unique(*markup); - } - } break; - - case mtpc_botInlineMessageMediaContact: { - auto &r = message->c_botInlineMessageMediaContact(); + geo, + qs(data.vvenue_id()), + qs(data.vprovider()), + qs(data.vtitle()), + qs(data.vaddress())); + }, [&](const MTPDgeoPointEmpty &) { + LOG(("Inline Error: Empty 'geo' in media-venue.")); + }); + }, [&](const MTPDbotInlineMessageMediaContact &data) { result->sendData = std::make_unique( session, - qs(r.vfirst_name()), - qs(r.vlast_name()), - qs(r.vphone_number())); - if (const auto markup = r.vreply_markup()) { - result->_mtpKeyboard = std::make_unique(*markup); - } - } break; + qs(data.vfirst_name()), + qs(data.vlast_name()), + qs(data.vphone_number())); + }, [&](const MTPDbotInlineMessageMediaInvoice &data) { + // #TODO payments + }); - default: { - badAttachment = true; - } break; - } - - if (badAttachment || !result->sendData || !result->sendData->isValid()) { + if (!result->sendData || !result->sendData->isValid()) { return nullptr; } + message->match([&](const auto &data) { + if (const auto markup = data.vreply_markup()) { + result->_mtpKeyboard = std::make_unique(*markup); + } + }); + if (const auto point = result->getLocationPoint()) { const auto scale = 1 + (cScale() * cIntRetinaFactor()) / 200; const auto zoom = 15 + (scale - 1); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index 2088615ee..6b3cae70e 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -97,7 +97,18 @@ SendDataCommon::SentMTPMessageFields SendText::getSentMessageFields() const { SendDataCommon::SentMTPMessageFields SendGeo::getSentMessageFields() const { SentMTPMessageFields result; - result.media = MTP_messageMediaGeo(_location.toMTP()); + if (_period) { + using Flag = MTPDmessageMediaGeoLive::Flag; + result.media = MTP_messageMediaGeoLive( + MTP_flags((_heading ? Flag::f_heading : Flag(0)) + | (_proximityNotificationRadius ? Flag::f_proximity_notification_radius : Flag(0))), + _location.toMTP(), + MTP_int(_heading.value_or(0)), + MTP_int(*_period), + MTP_int(_proximityNotificationRadius.value_or(0))); + } else { + result.media = MTP_messageMediaGeo(_location.toMTP()); + } return result; } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index 5d9780190..c977ebd1d 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -135,6 +135,18 @@ public: : SendDataCommon(session) , _location(point) { } + SendGeo( + not_null session, + const MTPDgeoPoint &point, + int period, + std::optional heading, + std::optional proximityNotificationRadius) + : SendDataCommon(session) + , _location(point) + , _period(period) + , _heading(heading) + , _proximityNotificationRadius(proximityNotificationRadius){ + } bool isValid() const override { return true; @@ -151,6 +163,9 @@ public: private: Data::LocationPoint _location; + std::optional _period; + std::optional _heading; + std::optional _proximityNotificationRadius; }; From 00c915e58d8cbe8a90f8fd80da1cfdda2797b69f Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 15:43:47 +0400 Subject: [PATCH 028/127] Add support for inline invoices. --- .../SourceFiles/history/history_service.cpp | 66 ++++++++++------ .../SourceFiles/history/history_service.h | 2 + .../view/history_view_service_message.cpp | 2 +- .../inline_bot_layout_internal.cpp | 25 ++++--- .../inline_bots/inline_bot_result.cpp | 75 +++++++++++-------- .../inline_bots/inline_bot_send_data.cpp | 10 +++ .../inline_bots/inline_bot_send_data.h | 22 ++++++ .../payments/payments_checkout_process.cpp | 7 +- .../payments/ui/payments_form_summary.cpp | 16 ++-- 9 files changed, 149 insertions(+), 76 deletions(-) diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 91385fca1..52d474981 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/notifications_manager.h" #include "window/window_session_controller.h" #include "storage/storage_shared_media.h" +#include "payments/payments_checkout_process.h" // CheckoutProcess::Start. #include "ui/text/format_values.h" #include "ui/text/text_options.h" @@ -522,18 +523,28 @@ bool HistoryService::updateDependent(bool force) { } if (!dependent->lnk) { - dependent->lnk = goToMessageClickHandler(history()->peer, dependent->msgId); + dependent->lnk = goToMessageClickHandler( + (dependent->peerId + ? history()->owner().peer(dependent->peerId) + : history()->peer), + dependent->msgId); } auto gotDependencyItem = false; if (!dependent->msg) { - dependent->msg = history()->owner().message(channelId(), dependent->msgId); + dependent->msg = history()->owner().message( + (dependent->peerId + ? peerToChannel(dependent->peerId) + : channelId()), + dependent->msgId); if (dependent->msg) { if (dependent->msg->isEmpty()) { // Really it is deleted. dependent->msg = nullptr; force = true; } else { - history()->owner().registerDependentMessage(this, dependent->msg); + history()->owner().registerDependentMessage( + this, + dependent->msg); gotDependencyItem = true; } } @@ -749,14 +760,14 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() { auto payment = Get(); auto invoiceTitle = [&] { - if (payment && payment->msg) { + if (payment->msg) { if (const auto media = payment->msg->media()) { if (const auto invoice = media->invoice()) { - return invoice->title; + return textcmdLink(1, invoice->title); } } return tr::lng_deleted_message(tr::now); - } else if (payment && payment->msgId) { + } else if (payment->msgId) { return tr::lng_contacts_loading(tr::now); } return QString(); @@ -766,6 +777,9 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() { result.text = tr::lng_action_payment_done(tr::now, lt_amount, payment->amount, lt_user, history()->peer->name); } else { result.text = tr::lng_action_payment_done_for(tr::now, lt_amount, payment->amount, lt_user, history()->peer->name, lt_invoice, invoiceTitle); + if (payment && payment->msg) { + result.links.push_back(payment->lnk); + } } return result; } @@ -958,9 +972,16 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { UpdateComponents(HistoryServicePayment::Bit()); const auto amount = data.vtotal_amount().v; const auto currency = qs(data.vcurrency()); - Get()->amount = Ui::FillAmountAndCurrency( - amount, - currency); + const auto payment = Get(); + const auto id = fullId(); + const auto owner = &history()->owner(); + payment->amount = Ui::FillAmountAndCurrency(amount, currency); + payment->invoiceLink = std::make_shared([=] { + using namespace Payments; + if (const auto item = owner->message(id)) { + CheckoutProcess::Start(item, Mode::Receipt); + } + }); } else if (message.vaction().type() == mtpc_messageActionGroupCall) { const auto &data = message.vaction().c_messageActionGroupCall(); if (data.vduration()) { @@ -1020,21 +1041,22 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { - const auto peer = data.vreply_to_peer_id() + const auto peerId = data.vreply_to_peer_id() ? peerFromMTP(*data.vreply_to_peer_id()) : history()->peer->id; - if (!peer || peer == history()->peer->id) { - if (message.vaction().type() == mtpc_messageActionPinMessage) { - UpdateComponents(HistoryServicePinned::Bit()); - } - if (const auto dependent = GetDependentData()) { - dependent->msgId = data.vreply_to_msg_id().v; - if (!updateDependent()) { - history()->session().api().requestMessageData( - history()->peer->asChannel(), - dependent->msgId, - HistoryDependentItemCallback(this)); - } + if (message.vaction().type() == mtpc_messageActionPinMessage) { + UpdateComponents(HistoryServicePinned::Bit()); + } + if (const auto dependent = GetDependentData()) { + dependent->peerId = (peerId != history()->peer->id) + ? peerId + : 0; + dependent->msgId = data.vreply_to_msg_id().v; + if (!updateDependent()) { + history()->session().api().requestMessageData( + history()->peer->asChannel(), + dependent->msgId, + HistoryDependentItemCallback(this)); } } }); diff --git a/Telegram/SourceFiles/history/history_service.h b/Telegram/SourceFiles/history/history_service.h index 7b7bcbab3..1f7c4854d 100644 --- a/Telegram/SourceFiles/history/history_service.h +++ b/Telegram/SourceFiles/history/history_service.h @@ -14,6 +14,7 @@ class Service; } // namespace HistoryView struct HistoryServiceDependentData { + PeerId peerId = 0; MsgId msgId = 0; HistoryItem *msg = nullptr; ClickHandlerPtr lnk; @@ -34,6 +35,7 @@ struct HistoryServicePayment : public RuntimeComponent , public HistoryServiceDependentData { QString amount; + ClickHandlerPtr invoiceLink; }; struct HistoryServiceSelfDestruct diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index b091c8573..693808180 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -523,7 +523,7 @@ TextState Service::textState(QPoint point, StateRequest request) const { if (!result.link && result.cursor == CursorState::Text && g.contains(point)) { - result.link = payment->lnk; + result.link = payment->invoiceLink; } } } else if (media) { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index a88bb5dfe..ca2af7d37 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -696,24 +696,24 @@ void Video::initDimensions() { const auto withThumb = withThumbnail(); _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; - int32 textWidth = _maxw - (withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0); - TextParseOptions titleOpts = { 0, _maxw, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; + const auto textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip); + TextParseOptions titleOpts = { 0, textWidth, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; auto title = TextUtilities::SingleLine(_result->getLayoutTitle()); if (title.isEmpty()) { title = tr::lng_media_video(tr::now); } _title.setText(st::semiboldTextStyle, title, titleOpts); - int32 titleHeight = qMin(_title.countHeight(_maxw), 2 * st::semiboldFont->height); + int32 titleHeight = qMin(_title.countHeight(textWidth), 2 * st::semiboldFont->height); int32 descriptionLines = withThumb ? (titleHeight > st::semiboldFont->height ? 1 : 2) : 3; - TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto }; + TextParseOptions descriptionOpts = { TextParseMultiline, textWidth, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto }; QString description = _result->getLayoutDescription(); if (description.isEmpty()) { description = _duration; } _description.setText(st::defaultTextStyle, description, descriptionOpts); - int32 descriptionHeight = qMin(_description.countHeight(_maxw), descriptionLines * st::normalFont->height); + int32 descriptionHeight = qMin(_description.countHeight(textWidth), descriptionLines * st::normalFont->height); _minh = st::inlineThumbSize; _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; @@ -1073,13 +1073,13 @@ Contact::Contact(not_null context, not_null result) void Contact::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int32 textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip); - TextParseOptions titleOpts = { 0, _maxw, st::semiboldFont->height, Qt::LayoutDirectionAuto }; + TextParseOptions titleOpts = { 0, textWidth, st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); - int32 titleHeight = qMin(_title.countHeight(_maxw), st::semiboldFont->height); + int32 titleHeight = qMin(_title.countHeight(textWidth), st::semiboldFont->height); - TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, st::normalFont->height, Qt::LayoutDirectionAuto }; + TextParseOptions descriptionOpts = { TextParseMultiline, textWidth, st::normalFont->height, Qt::LayoutDirectionAuto }; _description.setText(st::defaultTextStyle, _result->getLayoutDescription(), descriptionOpts); - int32 descriptionHeight = qMin(_description.countHeight(_maxw), st::normalFont->height); + int32 descriptionHeight = qMin(_description.countHeight(textWidth), st::normalFont->height); _minh = st::inlineFileSize; _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; @@ -1170,7 +1170,7 @@ Article::Article( void Article::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; - int32 textWidth = _maxw - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0); + int32 textWidth = _maxw - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : (st::emojiPanHeaderLeft - st::inlineResultsLeft)); TextParseOptions titleOpts = { 0, textWidth, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); int32 titleHeight = qMin(_title.countHeight(textWidth), 2 * st::semiboldFont->height); @@ -1192,8 +1192,9 @@ int32 Article::resizeGetHeight(int32 width) { if (_url) { _urlText = getResultUrl(); _urlWidth = st::normalFont->width(_urlText); - if (_urlWidth > _width - st::inlineThumbSize - st::inlineThumbSkip) { - _urlText = st::normalFont->elided(_urlText, _width - st::inlineThumbSize - st::inlineThumbSkip); + int32 textWidth = _width - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : (st::emojiPanHeaderLeft - st::inlineResultsLeft)); + if (_urlWidth > textWidth) { + _urlText = st::normalFont->elided(_urlText, textWidth); _urlWidth = st::normalFont->width(_urlText); } } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 22bcb7511..a605a7122 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -51,7 +51,7 @@ Result::Result(not_null session, const Creator &creator) std::unique_ptr Result::Create( not_null session, uint64 queryId, - const MTPBotInlineResult &mtpData) { + const MTPBotInlineResult &data) { using Type = Result::Type; const auto type = [&] { @@ -69,7 +69,7 @@ std::unique_ptr Result::Create( { u"geo"_q, Type::Geo }, { u"game"_q, Type::Game }, }; - const auto type = mtpData.match([](const auto &data) { + const auto type = data.match([](const auto &data) { return qs(data.vtype()); }); const auto i = kStringToTypeMap.find(type); @@ -82,16 +82,13 @@ std::unique_ptr Result::Create( auto result = std::make_unique( session, Creator{ queryId, type }); - const MTPBotInlineMessage *message = nullptr; - switch (mtpData.type()) { - case mtpc_botInlineResult: { - const auto &r = mtpData.c_botInlineResult(); - result->_id = qs(r.vid()); - result->_title = qs(r.vtitle().value_or_empty()); - result->_description = qs(r.vdescription().value_or_empty()); - result->_url = qs(r.vurl().value_or_empty()); + const auto message = data.match([&](const MTPDbotInlineResult &data) { + result->_id = qs(data.vid()); + result->_title = qs(data.vtitle().value_or_empty()); + result->_description = qs(data.vdescription().value_or_empty()); + result->_url = qs(data.vurl().value_or_empty()); const auto thumbMime = [&] { - if (const auto thumb = r.vthumb()) { + if (const auto thumb = data.vthumb()) { return thumb->match([&](const auto &data) { return data.vmime_type().v; }); @@ -99,7 +96,7 @@ std::unique_ptr Result::Create( return QByteArray(); }(); const auto contentMime = [&] { - if (const auto content = r.vcontent()) { + if (const auto content = data.vcontent()) { return content->match([&](const auto &data) { return data.vmime_type().v; }); @@ -109,49 +106,45 @@ std::unique_ptr Result::Create( const auto imageThumb = !thumbMime.isEmpty() && (thumbMime != kVideoThumbMime); const auto videoThumb = !thumbMime.isEmpty() && !imageThumb; - if (const auto content = r.vcontent()) { + if (const auto content = data.vcontent()) { result->_content_url = GetContentUrl(*content); if (result->_type == Type::Photo) { result->_photo = session->data().photoFromWeb( *content, (imageThumb - ? Images::FromWebDocument(*r.vthumb()) + ? Images::FromWebDocument(*data.vthumb()) : ImageLocation())); } else if (contentMime != "text/html"_q) { result->_document = session->data().documentFromWeb( result->adjustAttributes(*content), (imageThumb - ? Images::FromWebDocument(*r.vthumb()) + ? Images::FromWebDocument(*data.vthumb()) : ImageLocation()), (videoThumb - ? Images::FromWebDocument(*r.vthumb()) + ? Images::FromWebDocument(*data.vthumb()) : ImageLocation())); } } if (!result->_photo && !result->_document && imageThumb) { result->_thumbnail.update(result->_session, ImageWithLocation{ - .location = Images::FromWebDocument(*r.vthumb()) - }); + .location = Images::FromWebDocument(*data.vthumb()) + }); } - message = &r.vsend_message(); - } break; - case mtpc_botInlineMediaResult: { - const auto &r = mtpData.c_botInlineMediaResult(); - result->_id = qs(r.vid()); - result->_title = qs(r.vtitle().value_or_empty()); - result->_description = qs(r.vdescription().value_or_empty()); - if (const auto photo = r.vphoto()) { + return &data.vsend_message(); + }, [&](const MTPDbotInlineMediaResult &data) { + result->_id = qs(data.vid()); + result->_title = qs(data.vtitle().value_or_empty()); + result->_description = qs(data.vdescription().value_or_empty()); + if (const auto photo = data.vphoto()) { result->_photo = session->data().processPhoto(*photo); } - if (const auto document = r.vdocument()) { + if (const auto document = data.vdocument()) { result->_document = session->data().processDocument(*document); } - message = &r.vsend_message(); - } break; - } + return &data.vsend_message(); + }); if ((result->_photo && result->_photo->isNull()) - || (result->_document && result->_document->isNull()) - || !message) { + || (result->_document && result->_document->isNull())) { return nullptr; } @@ -248,7 +241,23 @@ std::unique_ptr Result::Create( qs(data.vlast_name()), qs(data.vphone_number())); }, [&](const MTPDbotInlineMessageMediaInvoice &data) { - // #TODO payments + using Flag = MTPDmessageMediaInvoice::Flag; + const auto media = MTP_messageMediaInvoice( + MTP_flags((data.is_shipping_address_requested() + ? Flag::f_shipping_address_requested + : Flag(0)) + | (data.is_test() ? Flag::f_test : Flag(0)) + | (data.vphoto() ? Flag::f_photo : Flag(0))), + data.vtitle(), + data.vdescription(), + data.vphoto() ? (*data.vphoto()) : MTPWebDocument(), + MTPint(), // receipt_msg_id + data.vcurrency(), + data.vtotal_amount(), + MTP_string(QString())); // start_param + result->sendData = std::make_unique( + session, + media); }); if (!result->sendData || !result->sendData->isValid()) { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index 6b3cae70e..07893c3e7 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -264,5 +264,15 @@ QString SendGame::getErrorOnSend( return error.value_or(QString()); } +auto SendInvoice::getSentMessageFields() const -> SentMTPMessageFields { + SentMTPMessageFields result; + result.media = _media; + return result; +} + +QString SendInvoice::getLayoutDescription(const Result *owner) const { + return qs(_media.c_messageMediaInvoice().vdescription()); +} + } // namespace internal } // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index c977ebd1d..012d80554 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -351,5 +351,27 @@ private: }; +class SendInvoice : public SendDataCommon { +public: + SendInvoice( + not_null session, + MTPMessageMedia media) + : SendDataCommon(session) + , _media(media) { + } + + bool isValid() const override { + return true; + } + + SentMTPMessageFields getSentMessageFields() const override; + + QString getLayoutDescription(const Result *owner) const override; + +private: + MTPMessageMedia _media; + +}; + } // namespace internal } // namespace InlineBots diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 42f287afb..aa4565a35 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -174,7 +174,8 @@ void CheckoutProcess::handleError(const Error &error) { } break; case Error::Type::Validate: { - if (_submitState == SubmitState::Validation) { + if (_submitState == SubmitState::Validation + || _submitState == SubmitState::Validated) { _submitState = SubmitState::None; } if (_initialSilentValidation) { @@ -281,7 +282,9 @@ void CheckoutProcess::panelCloseSure() { } void CheckoutProcess::panelSubmit() { - if (_submitState == SubmitState::Validation + if (_form->invoice().receipt.paid) { + panelCloseSure(); + } else if (_submitState == SubmitState::Validation || _submitState == SubmitState::Finishing) { return; } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 95d647ec9..32090c577 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -43,11 +43,15 @@ FormSummary::FormSummary( , _topShadow(this) , _bottomShadow(this) , _submit( - this, - tr::lng_payments_pay_amount( + this, + (_invoice.receipt.paid + ? tr::lng_about_done() + : tr::lng_payments_pay_amount( lt_amount, - rpl::single(formatAmount(computeTotalAmount()))), - st::paymentsPanelSubmit) { + rpl::single(formatAmount(computeTotalAmount())))), + (_invoice.receipt.paid + ? st::passportPanelSaveValue + : st::paymentsPanelSubmit)) { setupControls(); } @@ -220,13 +224,13 @@ void FormSummary::setupPrices(not_null layout) { Settings::AddSkip(layout, st::paymentsPricesTopSkip); if (_invoice.receipt) { - Settings::AddDivider(layout); - Settings::AddSkip(layout, st::paymentsPricesBottomSkip); addRow( tr::lng_payments_date_label(tr::now), langDateTime(base::unixtime::parse(_invoice.receipt.date)), true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); + Settings::AddDivider(layout); + Settings::AddSkip(layout, st::paymentsPricesBottomSkip); } const auto add = [&]( From 8cac76931e72bc61e97646828cf71de0be326130 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 18:49:41 +0400 Subject: [PATCH 029/127] Support adding tips in payments. --- Telegram/Resources/langs/lang.strings | 3 + .../payments/payments_checkout_process.cpp | 19 ++ .../payments/payments_checkout_process.h | 3 + .../SourceFiles/payments/payments_form.cpp | 25 +- Telegram/SourceFiles/payments/payments_form.h | 1 + .../payments/ui/payments_field.cpp | 3 + .../SourceFiles/payments/ui/payments_field.h | 1 + .../payments/ui/payments_form_summary.cpp | 37 ++- .../payments/ui/payments_panel.cpp | 47 ++++ .../SourceFiles/payments/ui/payments_panel.h | 1 + .../payments/ui/payments_panel_data.h | 5 +- .../payments/ui/payments_panel_delegate.h | 2 + .../SourceFiles/ui/text/format_values.cpp | 241 ++++++++++++++++-- 13 files changed, 355 insertions(+), 33 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2b6c73d04..97cab33a9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1889,6 +1889,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_billing_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_payments_tips_label" = "Tips"; +"lng_payments_tips_title" = "Tips"; +"lng_payments_tips_enter" = "Enter tips amount"; "lng_call_status_incoming" = "is calling you..."; "lng_call_status_connecting" = "connecting..."; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index aa4565a35..ac51ff9cf 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -254,6 +254,8 @@ void CheckoutProcess::handleError(const Error &error) { || id == u"PAYMENT_CREDENTIALS_INVALID"_q || id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) { showToast({ "Error: " + id + ". Your card has not been billed." }); + } else { + showToast({ "Error: " + id }); } break; default: Unexpected("Error type in CheckoutProcess::handleError."); @@ -284,6 +286,7 @@ void CheckoutProcess::panelCloseSure() { void CheckoutProcess::panelSubmit() { if (_form->invoice().receipt.paid) { panelCloseSure(); + return; } else if (_submitState == SubmitState::Validation || _submitState == SubmitState::Finishing) { return; @@ -437,6 +440,10 @@ void CheckoutProcess::chooseShippingOption() { _panel->chooseShippingOption(_form->shippingOptions()); } +void CheckoutProcess::chooseTips() { + _panel->chooseTips(_form->invoice()); +} + void CheckoutProcess::editPaymentMethod() { _panel->choosePaymentMethod(_form->paymentMethod().ui); } @@ -453,6 +460,18 @@ void CheckoutProcess::panelChangeShippingOption(const QString &id) { showForm(); } +void CheckoutProcess::panelChooseTips() { + if (_submitState != SubmitState::None) { + return; + } + chooseTips(); +} + +void CheckoutProcess::panelChangeTips(int64 value) { + _form->setTips(value); + showForm(); +} + void CheckoutProcess::panelValidateInformation( Ui::RequestedInformation data) { _form->validateInformation(data); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 794acdcec..53e85ed3c 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -67,6 +67,7 @@ private: void showInformationError(Ui::InformationField field); void showCardError(Ui::CardField field); void chooseShippingOption(); + void chooseTips(); void editPaymentMethod(); void performInitialSilentValidation(); @@ -84,6 +85,8 @@ private: void panelEditPhone() override; void panelChooseShippingOption() override; void panelChangeShippingOption(const QString &id) override; + void panelChooseTips() override; + void panelChangeTips(int64 value) override; void panelValidateInformation(Ui::RequestedInformation data) override; void panelValidateCard(Ui::UncheckedCardDetails data) override; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 7e183d625..ff9084857 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -45,6 +45,10 @@ namespace { }); } +[[nodiscard]] int64 ParsePriceAmount(uint64 value) { + return *reinterpret_cast(&value); +} + [[nodiscard]] std::vector ParsePrices( const MTPVector &data) { return ranges::views::all( @@ -53,7 +57,7 @@ namespace { return price.match([&](const MTPDlabeledPrice &data) { return Ui::LabeledPrice{ .label = qs(data.vlabel()), - .price = *reinterpret_cast(&data.vamount().v), + .price = ParsePriceAmount(data.vamount().v), }; }); }) | ranges::to_vector; @@ -290,6 +294,10 @@ void Form::processInvoice(const MTPDinvoice &data) { .cover = std::move(_invoice.cover), .prices = ParsePrices(data.vprices()), + .tipsMin = ParsePriceAmount(data.vmin_tip_amount().value_or_empty()), + .tipsMax = ParsePriceAmount(data.vmax_tip_amount().value_or_empty()), + .tipsSelected = ParsePriceAmount( + data.vdefault_tip_amount().value_or_empty()), .currency = qs(data.vcurrency()), .isNameRequested = data.is_name_requested(), @@ -330,8 +338,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { void Form::processDetails(const MTPDpayments_paymentReceipt &data) { _invoice.receipt = Ui::Receipt{ .date = data.vdate().v, - .totalAmount = *reinterpret_cast( - &data.vtotal_amount().v), + .totalAmount = ParsePriceAmount(data.vtotal_amount().v), .currency = qs(data.vcurrency()), .paid = true, }; @@ -440,7 +447,8 @@ void Form::submit() { : Flag::f_requested_info_id) | (_shippingOptions.selectedId.isEmpty() ? Flag(0) - : Flag::f_shipping_option_id)), + : Flag::f_shipping_option_id) + | (_invoice.tipsSelected ? Flag::f_tip_amount : Flag(0))), MTP_long(_details.formId), _peer->input, MTP_int(_msgId), @@ -449,7 +457,7 @@ void Form::submit() { MTP_inputPaymentCredentials( MTP_flags(0), MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))), - MTP_long(0) // #TODO payments tip_amount + MTP_long(_invoice.tipsSelected) )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { _updates.fire(PaymentFinished{ data.vupdates() }); @@ -668,6 +676,13 @@ void Form::setShippingOption(const QString &id) { _shippingOptions.selectedId = id; } +void Form::setTips(int64 value) { + _invoice.tipsSelected = std::clamp( + value, + _invoice.tipsMin, + _invoice.tipsMax); +} + void Form::processShippingOptions(const QVector &data) { _shippingOptions = Ui::ShippingOptions{ ranges::views::all( data diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index ca753eb31..ae37feece 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -173,6 +173,7 @@ public: void validateCard(const Ui::UncheckedCardDetails &details); void setPaymentCredentials(const NewCredentials &credentials); void setShippingOption(const QString &id); + void setTips(int64 value); void submit(); private: diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index fe958b207..20dc96160 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -46,6 +46,7 @@ namespace { case FieldType::CardCVC: case FieldType::Country: case FieldType::Phone: + case FieldType::PriceAmount: return true; } Unexpected("FieldType in Payments::Ui::UseMaskedField."); @@ -67,6 +68,7 @@ namespace { case FieldType::CardCVC: case FieldType::Country: case FieldType::Phone: + case FieldType::PriceAmount: return base::make_unique_q(parent); } Unexpected("FieldType in Payments::Ui::CreateWrap."); @@ -94,6 +96,7 @@ namespace { case FieldType::CardExpireDate: case FieldType::CardCVC: case FieldType::Country: + case FieldType::PriceAmount: return CreateChild( wrap.get(), st::paymentsField, diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index 2061ff533..351b1a35e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -29,6 +29,7 @@ enum class FieldType { Country, Phone, Email, + PriceAmount, }; struct FieldValidateRequest { diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 32090c577..6ee83fd83 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "data/data_countries.h" #include "lang/lang_keys.h" #include "base/unixtime.h" @@ -84,7 +85,7 @@ int64 FormSummary::computeTotalAmount() const { std::plus<>(), &LabeledPrice::price) : int64(0); - return total + shipping; + return total + shipping + _invoice.tipsSelected; } void FormSummary::setupControls() { @@ -193,12 +194,15 @@ void FormSummary::setupCover(not_null layout) { void FormSummary::setupPrices(not_null layout) { const auto addRow = [&]( const QString &label, - const QString &value, + const TextWithEntities &value, bool full = false) { const auto &st = full ? st::paymentsFullPriceAmount : st::paymentsPriceAmount; - const auto right = CreateChild(layout.get(), value, st); + const auto right = CreateChild( + layout.get(), + rpl::single(value), + st); const auto &padding = st::paymentsPricePadding; const auto left = layout->add( object_ptr( @@ -220,13 +224,14 @@ void FormSummary::setupPrices(not_null layout) { ) | rpl::start_with_next([=](int top, int width) { right->moveToRight(st::paymentsPricePadding.right(), top, width); }, right->lifetime()); + return right; }; Settings::AddSkip(layout, st::paymentsPricesTopSkip); if (_invoice.receipt) { addRow( tr::lng_payments_date_label(tr::now), - langDateTime(base::unixtime::parse(_invoice.receipt.date)), + { langDateTime(base::unixtime::parse(_invoice.receipt.date)) }, true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); Settings::AddDivider(layout); @@ -237,7 +242,7 @@ void FormSummary::setupPrices(not_null layout) { const QString &label, int64 amount, bool full = false) { - addRow(label, formatAmount(amount), full); + addRow(label, { formatAmount(amount) }, full); }; for (const auto &price : _invoice.prices) { add(price.label, price.price); @@ -251,7 +256,27 @@ void FormSummary::setupPrices(not_null layout) { add(price.label, price.price); } } - add(tr::lng_payments_total_label(tr::now), computeTotalAmount(), true); + + const auto computedTotal = computeTotalAmount(); + const auto total = _invoice.receipt.paid + ? _invoice.receipt.totalAmount + : computedTotal; + if (_invoice.receipt.paid) { + if (const auto tips = total - computedTotal) { + add(tr::lng_payments_tips_label(tr::now), tips); + } + } else if (_invoice.tipsMax > 0) { + const auto text = formatAmount(_invoice.tipsSelected); + const auto label = addRow( + tr::lng_payments_tips_label(tr::now), + Ui::Text::Link(text, "internal:edit_tips")); + label->setClickHandlerFilter([=](auto&&...) { + _delegate->panelChooseTips(); + return false; + }); + } + + add(tr::lng_payments_total_label(tr::now), total, true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index f5c7015cc..35ab4df37 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_edit_information.h" #include "payments/ui/payments_edit_card.h" #include "payments/ui/payments_panel_delegate.h" +#include "payments/ui/payments_field.h" #include "ui/widgets/separate_panel.h" #include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" @@ -19,6 +20,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_passport.h" namespace Payments::Ui { +namespace { + +[[nodiscard]] auto PriceAmountValidator(int64 min, int64 max) { + return [=](FieldValidateRequest request) { + return FieldValidateResult{ + .value = request.nowValue, + .position = request.nowPosition, + }; + }; +} + +} // namespace Panel::Panel(not_null delegate) : _delegate(delegate) @@ -127,6 +140,40 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { })); } +void Panel::chooseTips(const Invoice &invoice) { + const auto min = invoice.tipsMin; + const auto max = invoice.tipsMax; + const auto now = invoice.tipsSelected; + showBox(Box([=](not_null box) { + box->setTitle(tr::lng_payments_tips_title()); + + const auto row = box->lifetime().make_state( + box, + FieldConfig{ + .type = FieldType::PriceAmount, + .placeholder = tr::lng_payments_tips_enter(), + .value = QString::number(now), + .validator = PriceAmountValidator(min, max), + }); + box->setFocusCallback([=] { + row->setFocusFast(); + }); + box->addRow(row->ownedWidget()); + box->addRow(object_ptr(box, "Min: " + QString::number(min), st::defaultFlatLabel)); + box->addRow(object_ptr(box, "Max: " + QString::number(max), st::defaultFlatLabel)); + box->addButton(tr::lng_settings_save(), [=] { + const auto value = row->value().toLongLong(); + if (value < min || value > max) { + row->showError(); + } else { + _delegate->panelChangeTips(value); + box->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +} + void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { _widget->setTitle(tr::lng_payments_card_title()); if (method.native.supported) { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index e07703ba9..6d39159d3 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -63,6 +63,7 @@ public: const NativeMethodDetails &native, CardField field); void chooseShippingOption(const ShippingOptions &options); + void chooseTips(const Invoice &invoice); void choosePaymentMethod(const PaymentMethodDetails &method); bool showWebview(const QString &url, bool allowBack); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index e530bf6b7..13efeeff6 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -39,6 +39,9 @@ struct Invoice { Cover cover; std::vector prices; + int64 tipsMin = 0; + int64 tipsMax = 0; + int64 tipsSelected = 0; QString currency; Receipt receipt; @@ -53,7 +56,7 @@ struct Invoice { bool emailSentToProvider = false; [[nodiscard]] bool valid() const { - return !currency.isEmpty() && !prices.empty(); + return !currency.isEmpty() && (!prices.empty() || tipsMax); } [[nodiscard]] explicit operator bool() const { return valid(); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index 8b6124604..e3bf155e7 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -38,6 +38,8 @@ public: virtual void panelEditPhone() = 0; virtual void panelChooseShippingOption() = 0; virtual void panelChangeShippingOption(const QString &id) = 0; + virtual void panelChooseTips() = 0; + virtual void panelChangeTips(int64 value) = 0; virtual void panelValidateInformation(RequestedInformation data) = 0; virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index b614fe14c..4b41f6d84 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -10,6 +10,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include +#include +#include +#include + +[[nodiscard]] QString FormatWithSeparators( + double amount, + char decimal, + char thousands) { + Expects(decimal != 0); + + // Thanks https://stackoverflow.com/a/5058949 + struct FormattingHelper : std::numpunct { + FormattingHelper(char decimal, char thousands) + : decimal(decimal) + , thousands(thousands) { + } + + char do_decimal_point() const override { return decimal; } + char do_thousands_sep() const override { return thousands; } + + char decimal = '.'; + char thousands = ','; + }; + + auto stream = std::ostringstream(); + auto helper = FormattingHelper(decimal, thousands ? thousands : '?'); + stream.imbue(std::locale(stream.getloc(), &helper)); + stream << std::fixed << amount; + auto result = QString::fromStdString(stream.str()); + if (!thousands) { + result.replace('?', QString()); + } + return result; +} namespace Ui { @@ -126,13 +160,168 @@ QString FormatPlayedText(qint64 played, qint64 duration) { } QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { - static const auto ShortCurrencyNames = QMap{ - { u"USD"_q, QString::fromUtf8("\x24") }, - { u"GBP"_q, QString::fromUtf8("\xC2\xA3") }, - { u"EUR"_q, QString::fromUtf8("\xE2\x82\xAC") }, - { u"JPY"_q, QString::fromUtf8("\xC2\xA5") }, + struct Rule { + //const char *name = ""; + //const char *native = ""; + const char *international = ""; + char thousands = ','; + char decimal = '.'; + bool left = true; + bool space = false; }; - static const auto Denominators = QMap{ + static const auto kRules = std::vector>{ + { u"AED"_q, { "", ',', '.', true, true } }, + { u"AFN"_q, {} }, + { u"ALL"_q, { "", '.', ',', false } }, + { u"AMD"_q, { "", ',', '.', false, true } }, + { u"ARS"_q, { "", '.', ',', true, true } }, + { u"AUD"_q, { "AU$" } }, + { u"AZN"_q, { "", ' ', ',', false, true } }, + { u"BAM"_q, { "", '.', ',', false, true } }, + { u"BDT"_q, { "", ',', '.', true, true } }, + { u"BGN"_q, { "", ' ', ',', false, true } }, + { u"BND"_q, { "", '.', ',', } }, + { u"BOB"_q, { "", '.', ',', true, true } }, + { u"BRL"_q, { "R$", '.', ',', true, true } }, + { u"BHD"_q, { "", ',', '.', true, true } }, + { u"BYR"_q, { "", ' ', ',', false, true } }, + { u"CAD"_q, { "CA$" } }, + { u"CHF"_q, { "", '\'', '.', false, true } }, + { u"CLP"_q, { "", '.', ',', true, true } }, + { u"CNY"_q, { "\x43\x4E\xC2\xA5" } }, + { u"COP"_q, { "", '.', ',', true, true } }, + { u"CRC"_q, { "", '.', ',', } }, + { u"CZK"_q, { "", ' ', ',', false, true } }, + { u"DKK"_q, { "", '\0', ',', false, true } }, + { u"DOP"_q, {} }, + { u"DZD"_q, { "", ',', '.', true, true } }, + { u"EGP"_q, { "", ',', '.', true, true } }, + { u"EUR"_q, { "\xE2\x82\xAC", ' ', ',', false, true } }, + { u"GBP"_q, { "\xC2\xA3" } }, + { u"GEL"_q, { "", ' ', ',', false, true } }, + { u"GTQ"_q, {} }, + { u"HKD"_q, { "HK$" } }, + { u"HNL"_q, { "", ',', '.', true, true } }, + { u"HRK"_q, { "", '.', ',', false, true } }, + { u"HUF"_q, { "", ' ', ',', false, true } }, + { u"IDR"_q, { "", '.', ',', } }, + { u"ILS"_q, { "\xE2\x82\xAA", ',', '.', true, true } }, + { u"INR"_q, { "\xE2\x82\xB9" } }, + { u"ISK"_q, { "", '.', ',', false, true } }, + { u"JMD"_q, {} }, + { u"JPY"_q, { "\xC2\xA5" } }, + { u"KES"_q, {} }, + { u"KGS"_q, { "", ' ', '-', false, true } }, + { u"KRW"_q, { "\xE2\x82\xA9" } }, + { u"KZT"_q, { "", ' ', '-', } }, + { u"LBP"_q, { "", ',', '.', true, true } }, + { u"LKR"_q, { "", ',', '.', true, true } }, + { u"MAD"_q, { "", ',', '.', true, true } }, + { u"MDL"_q, { "", ',', '.', false, true } }, + { u"MNT"_q, { "", ' ', ',', } }, + { u"MUR"_q, {} }, + { u"MVR"_q, { "", ',', '.', false, true } }, + { u"MXN"_q, { "MX$" } }, + { u"MYR"_q, {} }, + { u"MZN"_q, {} }, + { u"NGN"_q, {} }, + { u"NIO"_q, { "", ',', '.', true, true } }, + { u"NOK"_q, { "", ' ', ',', true, true } }, + { u"NPR"_q, {} }, + { u"NZD"_q, { "NZ$" } }, + { u"PAB"_q, { "", ',', '.', true, true } }, + { u"PEN"_q, { "", ',', '.', true, true } }, + { u"PHP"_q, {} }, + { u"PKR"_q, {} }, + { u"PLN"_q, { "", ' ', ',', false, true } }, + { u"PYG"_q, { "", '.', ',', true, true } }, + { u"QAR"_q, { "", ',', '.', true, true } }, + { u"RON"_q, { "", '.', ',', false, true } }, + { u"RSD"_q, { "", '.', ',', false, true } }, + { u"RUB"_q, { "", ' ', ',', false, true } }, + { u"SAR"_q, { "", ',', '.', true, true } }, + { u"SEK"_q, { "", '.', ',', false, true } }, + { u"SGD"_q, {} }, + { u"THB"_q, { "\xE0\xB8\xBF" } }, + { u"TJS"_q, { "", ' ', ';', false, true } }, + { u"TRY"_q, { "", '.', ',', false, true } }, + { u"TTD"_q, {} }, + { u"TWD"_q, { "NT$" } }, + { u"TZS"_q, {} }, + { u"UAH"_q, { "", ' ', ',', false } }, + { u"UGX"_q, {} }, + { u"USD"_q, { "$" } }, + { u"UYU"_q, { "", '.', ',', true, true } }, + { u"UZS"_q, { "", ' ', ',', false, true } }, + { u"VND"_q, { "\xE2\x82\xAB", '.', ',', false, true } }, + { u"YER"_q, { "", ',', '.', true, true } }, + { u"ZAR"_q, { "", ',', '.', true, true } }, + { u"IRR"_q, { "", ',', '/', false, true } }, + { u"IQD"_q, { "", ',', '.', true, true } }, + { u"VEF"_q, { "", '.', ',', true, true } }, + { u"SYP"_q, { "", ',', '.', true, true } }, + + //{ u"VUV"_q, { "", ',', '.', false } }, + //{ u"WST"_q, {} }, + //{ u"XAF"_q, { "FCFA", ',', '.', false } }, + //{ u"XCD"_q, {} }, + //{ u"XOF"_q, { "CFA", ' ', ',', false } }, + //{ u"XPF"_q, { "", ',', '.', false } }, + //{ u"ZMW"_q, {} }, + //{ u"ANG"_q, {} }, + //{ u"RWF"_q, { "", ' ', ',', true, true } }, + //{ u"PGK"_q, {} }, + //{ u"TOP"_q, {} }, + //{ u"SBD"_q, {} }, + //{ u"SCR"_q, {} }, + //{ u"SHP"_q, {} }, + //{ u"SLL"_q, {} }, + //{ u"SOS"_q, {} }, + //{ u"SRD"_q, {} }, + //{ u"STD"_q, {} }, + //{ u"SVC"_q, {} }, + //{ u"SZL"_q, {} }, + //{ u"AOA"_q, {} }, + //{ u"AWG"_q, {} }, + //{ u"BBD"_q, {} }, + //{ u"BIF"_q, { "", ',', '.', false } }, + //{ u"BMD"_q, {} }, + //{ u"BSD"_q, {} }, + //{ u"BWP"_q, {} }, + //{ u"BZD"_q, {} }, + //{ u"CDF"_q, { "", ',', '.', false } }, + //{ u"CVE"_q, {} }, + //{ u"DJF"_q, { "", ',', '.', false } }, + //{ u"ETB"_q, {} }, + //{ u"FJD"_q, {} }, + //{ u"FKP"_q, {} }, + //{ u"GIP"_q, {} }, + //{ u"GMD"_q, { "", ',', '.', false } }, + //{ u"GNF"_q, { "", ',', '.', false } }, + //{ u"GYD"_q, {} }, + //{ u"HTG"_q, {} }, + //{ u"KHR"_q, { "", ',', '.', false } }, + //{ u"KMF"_q, { "", ',', '.', false } }, + //{ u"KYD"_q, {} }, + //{ u"LAK"_q, { "", ',', '.', false } }, + //{ u"LRD"_q, {} }, + //{ u"LSL"_q, { "", ',', '.', false } }, + //{ u"MGA"_q, {} }, + //{ u"MKD"_q, { "", '.', ',', false, true } }, + //{ u"MOP"_q, {} }, + //{ u"MWK"_q, {} }, + //{ u"NAD"_q, {} }, + }; + static const auto kRulesMap = [] { + // flat_multi_map_pair_type lacks some required constructors :( + auto &&pairs = kRules | ranges::views::transform([](auto &&pair) { + return base::flat_multi_map_pair_type( + pair.first, + pair.second); + }); + return base::flat_map(begin(pairs), end(pairs)); + }(); + static const auto kDenominators = base::flat_map{ { u"CLF"_q, 10000 }, { u"BHD"_q, 1000 }, { u"IQD"_q, 1000 }, @@ -163,21 +352,31 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"XPF"_q, 1 }, { u"MRO"_q, 10 }, }; - const auto currencyText = ShortCurrencyNames.value(currency, currency); - const auto denominator = Denominators.value(currency, 100); - const auto currencyValue = amount / float64(denominator); - const auto digits = [&] { - auto result = 0; - for (auto test = 1; test < denominator; test *= 10) { - ++result; - } - return result; - }(); - return QLocale::system().toCurrencyString(currencyValue, currencyText); - //auto amountBucks = amount / 100; - //auto amountCents = amount % 100; - //auto amountText = u"%1,%2").arg(amountBucks).arg(amountCents, 2, 10, QChar('0')); - //return currencyText + amountText; + const auto denominatorIt = kDenominators.find(currency); + const auto denominator = (denominatorIt != end(kDenominators)) + ? denominatorIt->second + : 100; + const auto value = amount / float64(denominator); + const auto ruleIt = kRulesMap.find(currency); + if (ruleIt == end(kRulesMap)) { + return QLocale::system().toCurrencyString(value, currency); + } + const auto &rule = ruleIt->second; + const auto name = (*rule.international) + ? QString::fromUtf8(rule.international) + : currency; + auto result = QString(); + if (rule.left) { + result.append(name); + if (rule.space) result.append(' '); + } + result.append( + FormatWithSeparators(value, rule.decimal, rule.thousands)); + if (!rule.left) { + if (rule.space) result.append(' '); + result.append(name); + } + return result; } QString ComposeNameString( From 21b502c754279f4025c034ecbf58c89c1bf67dba Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 19:24:14 +0400 Subject: [PATCH 030/127] Format money amount same way as server does. --- .../SourceFiles/ui/text/format_values.cpp | 149 ++++++++++-------- 1 file changed, 79 insertions(+), 70 deletions(-) diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index 4b41f6d84..7020b74b7 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -14,42 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -[[nodiscard]] QString FormatWithSeparators( - double amount, - char decimal, - char thousands) { - Expects(decimal != 0); - - // Thanks https://stackoverflow.com/a/5058949 - struct FormattingHelper : std::numpunct { - FormattingHelper(char decimal, char thousands) - : decimal(decimal) - , thousands(thousands) { - } - - char do_decimal_point() const override { return decimal; } - char do_thousands_sep() const override { return thousands; } - - char decimal = '.'; - char thousands = ','; - }; - - auto stream = std::ostringstream(); - auto helper = FormattingHelper(decimal, thousands ? thousands : '?'); - stream.imbue(std::locale(stream.getloc(), &helper)); - stream << std::fixed << amount; - auto result = QString::fromStdString(stream.str()); - if (!thousands) { - result.replace('?', QString()); - } - return result; -} - namespace Ui { - namespace { -QString FormatTextWithReadyAndTotal( +[[nodiscard]] QString FormatTextWithReadyAndTotal( tr::phrase phrase, qint64 ready, qint64 total) { @@ -77,6 +45,40 @@ QString FormatTextWithReadyAndTotal( return phrase(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb); } +[[nodiscard]] QString FormatWithSeparators( + double amount, + int precision, + char decimal, + char thousands) { + Expects(decimal != 0); + + // Thanks https://stackoverflow.com/a/5058949 + struct FormattingHelper : std::numpunct { + FormattingHelper(char decimal, char thousands) + : decimal(decimal) + , thousands(thousands) { + } + + char do_decimal_point() const override { return decimal; } + char do_thousands_sep() const override { return thousands; } + + char decimal = '.'; + char thousands = ','; + }; + + auto stream = std::ostringstream(); + stream.imbue(std::locale( + stream.getloc(), + new FormattingHelper(decimal, thousands ? thousands : '?'))); + stream.precision(precision); + stream << std::fixed << amount; + auto result = QString::fromStdString(stream.str()); + if (!thousands) { + result.replace('?', QString()); + } + return result; +} + } // namespace QString FormatSizeText(qint64 size) { @@ -321,42 +323,42 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { }); return base::flat_map(begin(pairs), end(pairs)); }(); - static const auto kDenominators = base::flat_map{ - { u"CLF"_q, 10000 }, - { u"BHD"_q, 1000 }, - { u"IQD"_q, 1000 }, - { u"JOD"_q, 1000 }, - { u"KWD"_q, 1000 }, - { u"LYD"_q, 1000 }, - { u"OMR"_q, 1000 }, - { u"TND"_q, 1000 }, - { u"BIF"_q, 1 }, - { u"BYR"_q, 1 }, - { u"CLP"_q, 1 }, - { u"CVE"_q, 1 }, - { u"DJF"_q, 1 }, - { u"GNF"_q, 1 }, - { u"ISK"_q, 1 }, - { u"JPY"_q, 1 }, - { u"KMF"_q, 1 }, - { u"KRW"_q, 1 }, - { u"MGA"_q, 1 }, - { u"PYG"_q, 1 }, - { u"RWF"_q, 1 }, - { u"UGX"_q, 1 }, - { u"UYI"_q, 1 }, - { u"VND"_q, 1 }, - { u"VUV"_q, 1 }, - { u"XAF"_q, 1 }, - { u"XOF"_q, 1 }, - { u"XPF"_q, 1 }, - { u"MRO"_q, 10 }, + static const auto kExponents = base::flat_map{ + { u"CLF"_q, 4 }, + { u"BHD"_q, 3 }, + { u"IQD"_q, 3 }, + { u"JOD"_q, 3 }, + { u"KWD"_q, 3 }, + { u"LYD"_q, 3 }, + { u"OMR"_q, 3 }, + { u"TND"_q, 3 }, + { u"BIF"_q, 0 }, + { u"BYR"_q, 0 }, + { u"CLP"_q, 0 }, + { u"CVE"_q, 0 }, + { u"DJF"_q, 0 }, + { u"GNF"_q, 0 }, + { u"ISK"_q, 0 }, + { u"JPY"_q, 0 }, + { u"KMF"_q, 0 }, + { u"KRW"_q, 0 }, + { u"MGA"_q, 0 }, + { u"PYG"_q, 0 }, + { u"RWF"_q, 0 }, + { u"UGX"_q, 0 }, + { u"UYI"_q, 0 }, + { u"VND"_q, 0 }, + { u"VUV"_q, 0 }, + { u"XAF"_q, 0 }, + { u"XOF"_q, 0 }, + { u"XPF"_q, 0 }, + { u"MRO"_q, 1 }, }; - const auto denominatorIt = kDenominators.find(currency); - const auto denominator = (denominatorIt != end(kDenominators)) - ? denominatorIt->second - : 100; - const auto value = amount / float64(denominator); + const auto exponentIt = kExponents.find(currency); + const auto exponent = (exponentIt != end(kExponents)) + ? exponentIt->second + : 2; + const auto value = amount / std::pow(10., exponent); const auto ruleIt = kRulesMap.find(currency); if (ruleIt == end(kRulesMap)) { return QLocale::system().toCurrencyString(value, currency); @@ -370,8 +372,15 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { result.append(name); if (rule.space) result.append(' '); } - result.append( - FormatWithSeparators(value, rule.decimal, rule.thousands)); + const auto precision = (currency != u"IRR"_q + || std::floor(value) != value) + ? exponent + : 0; + result.append(FormatWithSeparators( + value, + precision, + rule.decimal, + rule.thousands)); if (!rule.left) { if (rule.space) result.append(' '); result.append(name); From 619f70ab22cc62fa0e6347da8ba0aca8b19a170d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Mar 2021 21:18:39 +0400 Subject: [PATCH 031/127] Improve design of shipping option selection. --- Telegram/Resources/langs/lang.strings | 2 +- .../SourceFiles/payments/payments_form.cpp | 7 +- .../SourceFiles/payments/ui/payments.style | 11 +++ .../payments/ui/payments_edit_information.cpp | 8 ++ .../payments/ui/payments_edit_information.h | 2 + .../payments/ui/payments_form_summary.cpp | 13 ++-- .../payments/ui/payments_panel.cpp | 75 +++++++++++++++---- .../payments/ui/payments_panel_data.h | 2 + .../SourceFiles/ui/text/format_values.cpp | 11 ++- 9 files changed, 107 insertions(+), 24 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 97cab33a9..00d88d43c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1888,7 +1888,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_billing_country" = "Country"; "lng_payments_billing_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_payments_save_information" = "Save Information for future use"; "lng_payments_tips_label" = "Tips"; "lng_payments_tips_title" = "Tips"; "lng_payments_tips_enter" = "Enter tips amount"; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index ff9084857..62c671a8c 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -487,8 +487,9 @@ void Form::validateInformation(const Ui::RequestedInformation &information) { Assert(!_invoice.isEmailRequested || !information.email.isEmpty()); Assert(!_invoice.isPhoneRequested || !information.phone.isEmpty()); + using Flag = MTPpayments_ValidateRequestedInfo::Flag; _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( - MTP_flags(0), // #TODO payments save information + MTP_flags(information.save ? Flag::f_save : Flag(0)), _peer->input, MTP_int(_msgId), Serialize(information) @@ -684,7 +685,8 @@ void Form::setTips(int64 value) { } void Form::processShippingOptions(const QVector &data) { - _shippingOptions = Ui::ShippingOptions{ ranges::views::all( + const auto currency = _invoice.currency; + _shippingOptions = Ui::ShippingOptions{ currency, ranges::views::all( data ) | ranges::views::transform([](const MTPShippingOption &option) { return option.match([](const MTPDshippingOption &data) { @@ -695,6 +697,7 @@ void Form::processShippingOptions(const QVector &data) { }; }); }) | ranges::to_vector }; + _shippingOptions.currency = _invoice.currency; } } // namespace Payments diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 20e44354a..d6931c82f 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -59,6 +59,7 @@ paymentsIconShippingMethod: icon {{ "payments/payment_shipping", menuIconFg }}; paymentsField: defaultInputField; paymentsFieldPadding: margins(28px, 0px, 28px, 2px); +paymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px); paymentsExpireCvcSkip: 34px; paymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) { @@ -67,3 +68,13 @@ paymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) { minWidth: 240px; } paymentsBillingInformationTitlePadding: margins(28px, 26px, 28px, 1px); + +paymentsShippingMargin: margins(27px, 11px, 27px, 20px); +paymentsShippingLabel: FlatLabel(defaultFlatLabel) { + style: boxTextStyle; +} +paymentsShippingPrice: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} +paymentsShippingLabelPosition: point(43px, 8px); +paymentsShippingPricePosition: point(43px, 29px); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index d9b4f2562..b216d1d30 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/widgets/checkbox.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "lang/lang_keys.h" @@ -168,6 +169,12 @@ not_null EditInformation::setupContent() { .defaultPhone = _information.defaultPhone, }); } + _save = inner->add( + object_ptr( + inner, + tr::lng_payments_save_information(tr::now), + true), + st::paymentsSaveCheckboxPadding); return inner; } @@ -212,6 +219,7 @@ RequestedInformation EditInformation::collect() const { return { .defaultPhone = _information.defaultPhone, .defaultCountry = _information.defaultCountry, + .save = _save->checked(), .name = _name ? _name->value() : QString(), .phone = _phone ? _phone->value() : QString(), .email = _email ? _email->value() : QString(), diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h index 978b72b43..5864f8328 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -17,6 +17,7 @@ class FadeShadow; class RoundButton; class InputField; class MaskedInputField; +class Checkbox; } // namespace Ui namespace Payments::Ui { @@ -69,6 +70,7 @@ private: std::unique_ptr _name; std::unique_ptr _email; std::unique_ptr _phone; + Checkbox *_save = nullptr; InformationField _focusField = InformationField::ShippingStreet; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 6ee83fd83..8f441f6a7 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -22,6 +22,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_payments.h" #include "styles/style_passport.h" +namespace App { +QString formatPhone(QString phone); // #TODO +} // namespace App + namespace Payments::Ui { using namespace ::Ui; @@ -62,10 +66,7 @@ void FormSummary::updateThumbnail(const QImage &thumbnail) { } QString FormSummary::formatAmount(int64 amount) const { - const auto base = FillAmountAndCurrency( - std::abs(amount), - _invoice.currency); - return (amount < 0) ? (QString::fromUtf8("\xe2\x88\x92") + base) : base; + return FillAmountAndCurrency(amount, _invoice.currency); } int64 FormSummary::computeTotalAmount() const { @@ -352,7 +353,9 @@ void FormSummary::setupSections(not_null layout) { if (_invoice.isPhoneRequested) { add( tr::lng_payments_info_phone(), - _information.phone, + (_information.phone.isEmpty() + ? QString() + : App::formatPhone(_information.phone)), &st::paymentsIconPhone, [=] { _delegate->panelEditPhone(); }); } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 35ab4df37..48a19dea1 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -13,11 +13,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_panel_delegate.h" #include "payments/ui/payments_field.h" #include "ui/widgets/separate_panel.h" +#include "ui/widgets/checkbox.h" #include "ui/boxes/single_choice_box.h" +#include "ui/text/format_values.h" #include "lang/lang_keys.h" #include "webview/webview_embed.h" #include "styles/style_payments.h" #include "styles/style_passport.h" +#include "styles/style_layers.h" namespace Payments::Ui { namespace { @@ -119,23 +122,69 @@ void Panel::showInformationError( void Panel::chooseShippingOption(const ShippingOptions &options) { showBox(Box([=](not_null box) { - auto list = options.list | ranges::views::transform( - &ShippingOption::title - ) | ranges::to_vector; const auto i = ranges::find( options.list, options.selectedId, &ShippingOption::id); - const auto save = [=](int option) { - _delegate->panelChangeShippingOption(options.list[option].id); - }; - SingleChoiceBox(box, { - .title = tr::lng_payments_shipping_method(), - .options = list, - .initialSelection = (i != end(options.list) - ? (i - begin(options.list)) - : -1), - .callback = save, + const auto index = (i != end(options.list)) + ? (i - begin(options.list)) + : -1; + const auto group = std::make_shared(index); + + const auto layout = box->verticalLayout(); + auto counter = 0; + for (const auto &option : options.list) { + const auto index = counter++; + const auto button = layout->add( + object_ptr( + layout, + group, + index, + QString(), + st::defaultBoxCheckbox, + st::defaultRadio), + st::paymentsShippingMargin); + const auto label = CreateChild( + layout.get(), + option.title, + st::paymentsShippingLabel); + const auto total = ranges::accumulate( + option.prices, + int64(0), + std::plus<>(), + &LabeledPrice::price); + const auto price = CreateChild( + layout.get(), + FillAmountAndCurrency(total, options.currency), + st::paymentsShippingPrice); + const auto area = CreateChild(layout.get()); + area->setClickedCallback([=] { group->setValue(index); }); + button->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + label->move( + geometry.topLeft() + st::paymentsShippingLabelPosition); + price->move( + geometry.topLeft() + st::paymentsShippingPricePosition); + const auto right = geometry.x() + + st::paymentsShippingLabelPosition.x(); + area->setGeometry( + right, + geometry.y(), + std::max( + label->x() + label->width() - right, + price->x() + price->width() - right), + price->y() + price->height() - geometry.y()); + }, button->lifetime()); + } + + box->setTitle(tr::lng_payments_shipping_method()); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + group->setChangedCallback([=](int index) { + if (index >= 0) { + _delegate->panelChangeShippingOption( + options.list[index].id); + box->closeBox(); + } }); })); } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 13efeeff6..ce17f7c61 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -70,6 +70,7 @@ struct ShippingOption { }; struct ShippingOptions { + QString currency; std::vector list; QString selectedId; }; @@ -107,6 +108,7 @@ struct Address { struct RequestedInformation { QString defaultPhone; QString defaultCountry; + bool save = true; QString name; QString phone; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index 7020b74b7..edcca777c 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -354,14 +354,19 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"XPF"_q, 0 }, { u"MRO"_q, 1 }, }; + + const auto prefix = (amount < 0) + ? QString::fromUtf8("\xe2\x88\x92") + : QString(); + const auto exponentIt = kExponents.find(currency); const auto exponent = (exponentIt != end(kExponents)) ? exponentIt->second : 2; - const auto value = amount / std::pow(10., exponent); + const auto value = std::abs(amount) / std::pow(10., exponent); const auto ruleIt = kRulesMap.find(currency); if (ruleIt == end(kRulesMap)) { - return QLocale::system().toCurrencyString(value, currency); + return prefix + QLocale::system().toCurrencyString(value, currency); } const auto &rule = ruleIt->second; const auto name = (*rule.international) @@ -376,7 +381,7 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { || std::floor(value) != value) ? exponent : 0; - result.append(FormatWithSeparators( + result.append(prefix).append(FormatWithSeparators( value, precision, rule.decimal, From b08d9fe0b8e88b08eb78ec1050f0d8c4ab1dd3b1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 09:56:45 +0400 Subject: [PATCH 032/127] Reactivate window on payment close. --- Telegram/SourceFiles/facades.cpp | 5 +- .../SourceFiles/history/history_service.cpp | 12 ++++- .../payments/payments_checkout_process.cpp | 52 ++++++++++++------- .../payments/payments_checkout_process.h | 14 +++-- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 023f2da4f..c6535155d 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -122,7 +122,10 @@ void activateBotCommand( } break; case ButtonType::Buy: { - Payments::CheckoutProcess::Start(msg, Payments::Mode::Payment); + Payments::CheckoutProcess::Start( + msg, + Payments::Mode::Payment, + crl::guard(App::wnd(), [] { App::wnd()->activate(); })); } break; case ButtonType::Url: { diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 52d474981..e6d227278 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -28,8 +28,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_group_call.h" // Data::GroupCall::id(). #include "core/application.h" +#include "core/click_handler_types.h" #include "calls/calls_instance.h" // Core::App().calls().joinGroupCall. #include "window/notifications_manager.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "storage/storage_shared_media.h" #include "payments/payments_checkout_process.h" // CheckoutProcess::Start. @@ -976,10 +978,16 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { const auto id = fullId(); const auto owner = &history()->owner(); payment->amount = Ui::FillAmountAndCurrency(amount, currency); - payment->invoiceLink = std::make_shared([=] { + payment->invoiceLink = std::make_shared([=]( + ClickContext context) { using namespace Payments; + const auto my = context.other.value(); + const auto weak = my.sessionWindow; if (const auto item = owner->message(id)) { - CheckoutProcess::Start(item, Mode::Receipt); + CheckoutProcess::Start( + item, + Mode::Receipt, + crl::guard(weak, [=] { weak->window().activate(); })); } }); } else if (message.vaction().type() == mtpc_messageActionGroupCall) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index ac51ff9cf..6eb4ab7a4 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -20,10 +20,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/file_utilities.h" // File::OpenUrl. #include "apiwrap.h" -// #TODO payments errors -#include "mainwindow.h" -#include "ui/toasts/common_toasts.h" - #include #include #include @@ -57,7 +53,10 @@ base::flat_map, SessionProcesses> Processes; } // namespace -void CheckoutProcess::Start(not_null item, Mode mode) { +void CheckoutProcess::Start( + not_null item, + Mode mode, + Fn reactivate) { auto &processes = LookupSessionProcesses(item); const auto session = &item->history()->session(); const auto media = item->media(); @@ -76,6 +75,7 @@ void CheckoutProcess::Start(not_null item, Mode mode) { } const auto i = processes.map.find(id); if (i != end(processes.map)) { + i->second->setReactivateCallback(std::move(reactivate)); i->second->requestActivate(); return; } @@ -85,6 +85,7 @@ void CheckoutProcess::Start(not_null item, Mode mode) { item->history()->peer, id.msg, mode, + std::move(reactivate), PrivateTag{})).first; j->second->requestActivate(); } @@ -93,10 +94,12 @@ CheckoutProcess::CheckoutProcess( not_null peer, MsgId itemId, Mode mode, + Fn reactivate, PrivateTag) : _session(&peer->session()) , _form(std::make_unique(peer, itemId, (mode == Mode::Receipt))) -, _panel(std::make_unique(panelDelegate())) { +, _panel(std::make_unique(panelDelegate())) +, _reactivate(std::move(reactivate)) { _form->updates( ) | rpl::start_with_next([=](const FormUpdate &update) { handleFormUpdate(update); @@ -111,6 +114,10 @@ CheckoutProcess::CheckoutProcess( CheckoutProcess::~CheckoutProcess() { } +void CheckoutProcess::setReactivateCallback(Fn reactivate) { + _reactivate = std::move(reactivate); +} + void CheckoutProcess::requestActivate() { _panel->requestActivate(); } @@ -141,13 +148,14 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { }, [&](const VerificationNeeded &data) { if (!_panel->showWebview(data.url, false)) { File::OpenUrl(data.url); - panelCloseSure(); + close(); } }, [&](const PaymentFinished &data) { const auto weak = base::make_weak(this); _session->api().applyUpdates(data.updates); if (weak) { - panelCloseSure(); + closeAndReactivate(); + if (_reactivate) _reactivate(); } }, [&](const Error &error) { handleError(error); @@ -156,13 +164,8 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { void CheckoutProcess::handleError(const Error &error) { const auto showToast = [&](const TextWithEntities &text) { - if (_panel) { - _panel->requestActivate(); - _panel->showToast(text); - } else { - App::wnd()->activate(); - Ui::ShowMultilineToast({ .text = text }); - } + _panel->requestActivate(); + _panel->showToast(text); }; const auto &id = error.id; switch (error.type) { @@ -267,6 +270,18 @@ void CheckoutProcess::panelRequestClose() { } void CheckoutProcess::panelCloseSure() { + closeAndReactivate(); +} + +void CheckoutProcess::closeAndReactivate() { + const auto reactivate = std::move(_reactivate); + close(); + if (reactivate) { + reactivate(); + } +} + +void CheckoutProcess::close() { const auto i = Processes.find(_session); if (i == end(Processes)) { return; @@ -285,7 +300,7 @@ void CheckoutProcess::panelCloseSure() { void CheckoutProcess::panelSubmit() { if (_form->invoice().receipt.paid) { - panelCloseSure(); + closeAndReactivate(); return; } else if (_submitState == SubmitState::Validation || _submitState == SubmitState::Finishing) { @@ -366,10 +381,7 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { if (Core::TryConvertUrlToLocal(uri) == uri) { return true; } - crl::on_main(this, [=] { - panelCloseSure(); - App::wnd()->activate(); - }); + crl::on_main(this, [=] { closeAndReactivate(); }); return false; } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 53e85ed3c..ea5ca02e1 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -39,17 +39,19 @@ class CheckoutProcess final struct PrivateTag {}; public: - static void Start(not_null item, Mode mode); + static void Start( + not_null item, + Mode mode, + Fn reactivate); CheckoutProcess( not_null peer, MsgId itemId, Mode mode, + Fn reactivate, PrivateTag); ~CheckoutProcess(); - void requestActivate(); - private: enum class SubmitState { None, @@ -59,6 +61,11 @@ private: }; [[nodiscard]] not_null panelDelegate(); + void setReactivateCallback(Fn reactivate); + void requestActivate(); + void closeAndReactivate(); + void close(); + void handleFormUpdate(const FormUpdate &update); void handleError(const Error &error); @@ -97,6 +104,7 @@ private: const not_null _session; const std::unique_ptr _form; const std::unique_ptr _panel; + Fn _reactivate; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; From f09a468a7c470a98821611ddb92dfe41e7bbf2e1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 10:19:14 +0400 Subject: [PATCH 033/127] Improve design of payment bottom buttons. --- .../payments/payments_checkout_process.cpp | 9 +++- .../payments/payments_checkout_process.h | 1 + .../SourceFiles/payments/ui/payments.style | 9 ++-- .../payments/ui/payments_edit_card.cpp | 31 +++++++---- .../payments/ui/payments_edit_card.h | 3 +- .../payments/ui/payments_edit_information.cpp | 31 +++++++---- .../payments/ui/payments_edit_information.h | 3 +- .../payments/ui/payments_form_summary.cpp | 54 +++++++++++++------ .../payments/ui/payments_form_summary.h | 1 + .../payments/ui/payments_panel_delegate.h | 1 + .../SourceFiles/ui/text/format_values.cpp | 4 +- 11 files changed, 103 insertions(+), 44 deletions(-) diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 6eb4ab7a4..ff8692009 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -106,7 +106,7 @@ CheckoutProcess::CheckoutProcess( }, _lifetime); _panel->backRequests( ) | rpl::start_with_next([=] { - showForm(); + panelCancelEdit(); }, _panel->lifetime()); showForm(); } @@ -385,6 +385,13 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { return false; } +void CheckoutProcess::panelCancelEdit() { + if (_submitState != SubmitState::None) { + return; + } + showForm(); +} + void CheckoutProcess::panelEditPaymentMethod() { if (_submitState != SubmitState::None && _submitState != SubmitState::Validated) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index ea5ca02e1..06ca7ca7e 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -85,6 +85,7 @@ private: void panelWebviewMessage(const QJsonDocument &message) override; bool panelWebviewNavigationAttempt(const QString &uri) override; + void panelCancelEdit() override; void panelEditPaymentMethod() override; void panelEditShippingInformation() override; void panelEditName() override; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index d6931c82f..3dbc9eeaa 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -9,12 +9,13 @@ using "ui/basic.style"; using "info/info.style"; +paymentsPanelButton: defaultBoxButton; paymentsPanelSubmit: RoundButton(defaultActiveButton) { - width: 0px; - height: 49px; - padding: margins(0px, -3px, 0px, 0px); - textTop: 16px; + width: -36px; + height: 36px; + font: boxButtonFont; } +paymentsPanelPadding: margins(8px, 12px, 15px, 12px); paymentsCoverPadding: margins(26px, 0px, 26px, 13px); paymentsDescription: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 583e73d10..4abdf760d 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -208,10 +208,14 @@ EditCard::EditCard( , _scroll(this, st::passportPanelScroll) , _topShadow(this) , _bottomShadow(this) -, _done( +, _submit( + this, + tr::lng_about_done(), + st::paymentsPanelButton) +, _cancel( this, - tr::lng_about_done(), - st::passportPanelSaveValue) { + tr::lng_cancel(), + st::paymentsPanelButton) { setupControls(); } @@ -241,9 +245,12 @@ void EditCard::showError(CardField field) { void EditCard::setupControls() { const auto inner = setupContent(); - _done->addClickHandler([=] { + _submit->addClickHandler([=] { _delegate->panelValidateCard(collect()); }); + _cancel->addClickHandler([=] { + _delegate->panelCancelEdit(); + }); using namespace rpl::mappers; @@ -364,14 +371,20 @@ void EditCard::focusInEvent(QFocusEvent *e) { } void EditCard::updateControlsGeometry() { - const auto submitTop = height() - _done->height(); - _scroll->setGeometry(0, 0, width(), submitTop); + const auto &padding = st::paymentsPanelPadding; + const auto buttonsHeight = padding.top() + + _cancel->height() + + padding.bottom(); + const auto buttonsTop = height() - buttonsHeight; + _scroll->setGeometry(0, 0, width(), buttonsTop); _topShadow->resizeToWidth(width()); _topShadow->moveToLeft(0, 0); _bottomShadow->resizeToWidth(width()); - _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); - _done->setFullWidth(width()); - _done->moveToLeft(0, submitTop); + _bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth); + auto right = padding.right(); + _submit->moveToRight(right, buttonsTop + padding.top()); + right += _submit->width() + padding.left(); + _cancel->moveToRight(right, buttonsTop + padding.top()); _scroll->updateBars(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h index 742bdbf34..da6a23c4c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -53,7 +53,8 @@ private: object_ptr _scroll; object_ptr _topShadow; object_ptr _bottomShadow; - object_ptr _done; + object_ptr _submit; + object_ptr _cancel; std::unique_ptr _number; std::unique_ptr _cvc; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index b216d1d30..04940544e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -44,10 +44,14 @@ EditInformation::EditInformation( , _scroll(this, st::passportPanelScroll) , _topShadow(this) , _bottomShadow(this) -, _done( +, _submit( + this, + tr::lng_settings_save(), + st::paymentsPanelButton) +, _cancel( this, - tr::lng_about_done(), - st::passportPanelSaveValue) { + tr::lng_cancel(), + st::paymentsPanelButton) { setupControls(); } @@ -79,9 +83,12 @@ void EditInformation::showError(InformationField field) { void EditInformation::setupControls() { const auto inner = setupContent(); - _done->addClickHandler([=] { + _submit->addClickHandler([=] { _delegate->panelValidateInformation(collect()); }); + _cancel->addClickHandler([=] { + _delegate->panelCancelEdit(); + }); using namespace rpl::mappers; @@ -189,14 +196,20 @@ void EditInformation::focusInEvent(QFocusEvent *e) { } void EditInformation::updateControlsGeometry() { - const auto submitTop = height() - _done->height(); - _scroll->setGeometry(0, 0, width(), submitTop); + const auto &padding = st::paymentsPanelPadding; + const auto buttonsHeight = padding.top() + + _cancel->height() + + padding.bottom(); + const auto buttonsTop = height() - buttonsHeight; + _scroll->setGeometry(0, 0, width(), buttonsTop); _topShadow->resizeToWidth(width()); _topShadow->moveToLeft(0, 0); _bottomShadow->resizeToWidth(width()); - _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); - _done->setFullWidth(width()); - _done->moveToLeft(0, submitTop); + _bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth); + auto right = padding.right(); + _submit->moveToRight(right, buttonsTop + padding.top()); + right += _submit->width() + padding.left(); + _cancel->moveToRight(right, buttonsTop + padding.top()); _scroll->updateBars(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h index 5864f8328..6755939a0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -59,7 +59,8 @@ private: object_ptr _scroll; object_ptr _topShadow; object_ptr _bottomShadow; - object_ptr _done; + object_ptr _submit; + object_ptr _cancel; std::unique_ptr _street1; std::unique_ptr _street2; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 8f441f6a7..b1aa3a2b6 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -47,16 +47,20 @@ FormSummary::FormSummary( , _scroll(this, st::passportPanelScroll) , _topShadow(this) , _bottomShadow(this) -, _submit( - this, - (_invoice.receipt.paid - ? tr::lng_about_done() - : tr::lng_payments_pay_amount( +, _submit(_invoice.receipt.paid + ? object_ptr(nullptr) + : object_ptr( + this, + tr::lng_payments_pay_amount( lt_amount, - rpl::single(formatAmount(computeTotalAmount())))), - (_invoice.receipt.paid - ? st::passportPanelSaveValue - : st::paymentsPanelSubmit)) { + rpl::single(formatAmount(computeTotalAmount()))), + st::paymentsPanelSubmit)) +, _cancel( + this, + (_invoice.receipt.paid + ? tr::lng_about_done() + : tr::lng_cancel()), + st::paymentsPanelButton) { setupControls(); } @@ -92,11 +96,19 @@ int64 FormSummary::computeTotalAmount() const { void FormSummary::setupControls() { const auto inner = setupContent(); - _submit->addClickHandler([=] { - _delegate->panelSubmit(); + if (_submit) { + _submit->addClickHandler([=] { + _delegate->panelSubmit(); + }); + } + _cancel->addClickHandler([=] { + _delegate->panelRequestClose(); }); if (!_invoice) { - _submit->hide(); + if (_submit) { + _submit->hide(); + } + _cancel->hide(); } using namespace rpl::mappers; @@ -387,14 +399,22 @@ void FormSummary::resizeEvent(QResizeEvent *e) { } void FormSummary::updateControlsGeometry() { - const auto submitTop = height() - _submit->height(); - _scroll->setGeometry(0, 0, width(), submitTop); + const auto &padding = st::paymentsPanelPadding; + const auto buttonsHeight = padding.top() + + _cancel->height() + + padding.bottom(); + const auto buttonsTop = height() - buttonsHeight; + _scroll->setGeometry(0, 0, width(), buttonsTop); _topShadow->resizeToWidth(width()); _topShadow->moveToLeft(0, 0); _bottomShadow->resizeToWidth(width()); - _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); - _submit->setFullWidth(width()); - _submit->moveToLeft(0, submitTop); + _bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth); + auto right = padding.right(); + if (_submit) { + _submit->moveToRight(right, buttonsTop + padding.top()); + right += _submit->width() + padding.left(); + } + _cancel->moveToRight(right, buttonsTop + padding.top()); _scroll->updateBars(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 8ce6bcd4c..7db3e4b16 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -58,6 +58,7 @@ private: object_ptr _topShadow; object_ptr _bottomShadow; object_ptr _submit; + object_ptr _cancel; rpl::event_stream _thumbnails; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index e3bf155e7..cce968fe1 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -31,6 +31,7 @@ public: virtual void panelWebviewMessage(const QJsonDocument &message) = 0; virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; + virtual void panelCancelEdit() = 0; virtual void panelEditPaymentMethod() = 0; virtual void panelEditShippingInformation() = 0; virtual void panelEditName() = 0; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index edcca777c..9294cc0bc 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -372,7 +372,7 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { const auto name = (*rule.international) ? QString::fromUtf8(rule.international) : currency; - auto result = QString(); + auto result = prefix; if (rule.left) { result.append(name); if (rule.space) result.append(' '); @@ -381,7 +381,7 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { || std::floor(value) != value) ? exponent : 0; - result.append(prefix).append(FormatWithSeparators( + result.append(FormatWithSeparators( value, precision, rule.decimal, From 2e589931813c36a32129fa1bb0ee3c59648374b1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 14:00:13 +0400 Subject: [PATCH 034/127] Use title/description from paymentReceipt. --- .../SourceFiles/history/history_service.cpp | 20 +++++++++++++++---- .../SourceFiles/history/history_service.h | 2 ++ .../payments/payments_checkout_process.cpp | 1 - .../SourceFiles/payments/payments_form.cpp | 18 +++++++++++++++-- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index e6d227278..132b3e3c8 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -768,9 +768,9 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() { return textcmdLink(1, invoice->title); } } - return tr::lng_deleted_message(tr::now); + return QString();// tr::lng_deleted_message(tr::now); } else if (payment->msgId) { - return tr::lng_contacts_loading(tr::now); + return QString();// tr::lng_contacts_loading(tr::now); } return QString(); }(); @@ -1062,7 +1062,10 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { dependent->msgId = data.vreply_to_msg_id().v; if (!updateDependent()) { history()->session().api().requestMessageData( - history()->peer->asChannel(), + (peerIsChannel(dependent->peerId) + ? history()->owner().channel( + peerToChannel(dependent->peerId)).get() + : history()->peer->asChannel()), dependent->msgId, HistoryDependentItemCallback(this)); } @@ -1139,11 +1142,20 @@ void HistoryService::updateText(PreparedText &&text) { void HistoryService::clearDependency() { if (const auto dependent = GetDependentData()) { if (dependent->msg) { - history()->owner().unregisterDependentMessage(this, dependent->msg); + history()->owner().unregisterDependentMessage( + this, + dependent->msg); + dependent->msg = nullptr; + dependent->msgId = 0; } } } +void HistoryService::dependencyItemRemoved(HistoryItem *dependency) { + clearDependency(); + updateDependentText(); +} + HistoryService::~HistoryService() { clearDependency(); _media.reset(); diff --git a/Telegram/SourceFiles/history/history_service.h b/Telegram/SourceFiles/history/history_service.h index 1f7c4854d..e7bb8ee70 100644 --- a/Telegram/SourceFiles/history/history_service.h +++ b/Telegram/SourceFiles/history/history_service.h @@ -103,6 +103,8 @@ public: Storage::SharedMediaTypesMask sharedMediaTypes() const override; + void dependencyItemRemoved(HistoryItem *dependency) override; + bool needCheck() const override; bool serviceMsg() const override { return true; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index ff8692009..376e3b36d 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -155,7 +155,6 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _session->api().applyUpdates(data.updates); if (weak) { closeAndReactivate(); - if (_reactivate) _reactivate(); } }, [&](const Error &error) { handleError(error); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 62c671a8c..63597878e 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -116,8 +116,8 @@ void Form::fillInvoiceFromMessage() { if (const auto item = _session->data().message(id)) { const auto media = [&] { if (const auto payment = item->Get()) { - if (payment->msg) { - return payment->msg->media(); + if (const auto invoice = payment->msg) { + return invoice->media(); } } return item->media(); @@ -346,6 +346,20 @@ void Form::processDetails(const MTPDpayments_paymentReceipt &data) { .botId = data.vbot_id().v, .providerId = data.vprovider_id().v, }; + if (_invoice.cover.title.isEmpty() + && _invoice.cover.description.isEmpty() + && _invoice.cover.thumbnail.isNull() + && !_thumbnailLoadProcess) { + _invoice.cover = Ui::Cover{ + .title = qs(data.vtitle()), + .description = qs(data.vdescription()), + }; + if (const auto web = data.vphoto()) { + if (const auto photo = _session->data().photoFromWeb(*web, {})) { + loadThumbnail(photo); + } + } + } if (_details.botId) { if (const auto bot = _session->data().userLoaded(_details.botId)) { _invoice.cover.seller = bot->name; From 663db64688cf5d6dddd6c6525b7d7467c5a6bfd7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 21:15:49 +0400 Subject: [PATCH 035/127] Allow saving and using saved credentials. --- Telegram/Resources/langs/lang.strings | 6 +- Telegram/SourceFiles/boxes/passcode_box.cpp | 28 ++-- Telegram/SourceFiles/boxes/passcode_box.h | 2 + Telegram/SourceFiles/main/main_session.cpp | 16 +++ Telegram/SourceFiles/main/main_session.h | 6 + .../SourceFiles/mtproto/mtproto_response.h | 6 +- .../payments/payments_checkout_process.cpp | 134 +++++++++++++++--- .../payments/payments_checkout_process.h | 28 +++- .../SourceFiles/payments/payments_form.cpp | 82 +++++++++-- Telegram/SourceFiles/payments/payments_form.h | 21 ++- .../payments/ui/payments_edit_card.cpp | 11 +- .../payments/ui/payments_edit_card.h | 2 + .../payments/ui/payments_panel.cpp | 47 +++++- .../SourceFiles/payments/ui/payments_panel.h | 5 + .../payments/ui/payments_panel_data.h | 2 + .../payments/ui/payments_panel_delegate.h | 9 +- 16 files changed, 350 insertions(+), 55 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 00d88d43c..7b7241944 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1880,15 +1880,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_info_email" = "Email"; "lng_payments_info_phone" = "Phone"; "lng_payments_shipping_address_title" = "Shipping Information"; -"lng_payments_save_shipping_about" = "You can save your shipping information for future use."; "lng_payments_card_title" = "New Card"; "lng_payments_card_number" = "Card Number"; "lng_payments_card_holder" = "Cardholder name"; "lng_payments_billing_address" = "Billing Information"; "lng_payments_billing_country" = "Country"; "lng_payments_billing_zip_code" = "Zip Code"; -"lng_payments_save_payment_about" = "You can save your payment information for future use."; "lng_payments_save_information" = "Save Information for future use"; +"lng_payments_need_password" = "You can save your payment information for future use. Please turn on Two-Step Verification to enable this."; +"lng_payments_password_title" = "Payment Confirmation"; +"lng_payments_password_description" = "Your card {card} is on file. To pay with this card, please enter your 2-Step-Verification password."; +"lng_payments_password_submit" = "Pay"; "lng_payments_tips_label" = "Tips"; "lng_payments_tips_title" = "Tips"; "lng_payments_tips_enter" = "Enter tips amount"; diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index bf664d060..83ac3f351 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -361,7 +361,11 @@ void PasscodeBox::closeReplacedBy() { } void PasscodeBox::setPasswordFail(const MTP::Error &error) { - if (MTP::IsFloodError(error)) { + setPasswordFail(error.type()); +} + +void PasscodeBox::setPasswordFail(const QString &type) { + if (MTP::IsFloodError(type)) { closeReplacedBy(); _setRequest = 0; @@ -378,20 +382,19 @@ void PasscodeBox::setPasswordFail(const MTP::Error &error) { closeReplacedBy(); _setRequest = 0; - const auto &err = error.type(); - if (err == qstr("PASSWORD_HASH_INVALID") - || err == qstr("SRP_PASSWORD_CHANGED")) { + if (type == qstr("PASSWORD_HASH_INVALID") + || type == qstr("SRP_PASSWORD_CHANGED")) { if (_oldPasscode->isHidden()) { _passwordReloadNeeded.fire({}); closeBox(); } else { badOldPasscode(); } - } else if (err == qstr("SRP_ID_INVALID")) { + } else if (type == qstr("SRP_ID_INVALID")) { handleSrpIdInvalid(); - //} else if (err == qstr("NEW_PASSWORD_BAD")) { - //} else if (err == qstr("NEW_SALT_INVALID")) { - } else if (err == qstr("EMAIL_INVALID")) { + //} else if (type == qstr("NEW_PASSWORD_BAD")) { + //} else if (type == qstr("NEW_SALT_INVALID")) { + } else if (type == qstr("EMAIL_INVALID")) { _emailError = tr::lng_cloud_password_bad_email(tr::now); _recoverEmail->setFocus(); _recoverEmail->showError(); @@ -682,12 +685,15 @@ void PasscodeBox::serverError() { } bool PasscodeBox::handleCustomCheckError(const MTP::Error &error) { - const auto &type = error.type(); - if (MTP::IsFloodError(error) + return handleCustomCheckError(error.type()); +} + +bool PasscodeBox::handleCustomCheckError(const QString &type) { + if (MTP::IsFloodError(type) || type == qstr("PASSWORD_HASH_INVALID") || type == qstr("SRP_PASSWORD_CHANGED") || type == qstr("SRP_ID_INVALID")) { - setPasswordFail(error); + setPasswordFail(type); return true; } return false; diff --git a/Telegram/SourceFiles/boxes/passcode_box.h b/Telegram/SourceFiles/boxes/passcode_box.h index 80644c2be..875bed383 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.h +++ b/Telegram/SourceFiles/boxes/passcode_box.h @@ -56,6 +56,7 @@ public: rpl::producer<> clearUnconfirmedPassword() const; bool handleCustomCheckError(const MTP::Error &error); + bool handleCustomCheckError(const QString &type); protected: void prepare() override; @@ -82,6 +83,7 @@ private: void setPasswordDone(const QByteArray &newPasswordBytes); void setPasswordFail(const MTP::Error &error); + void setPasswordFail(const QString &type); void setPasswordFail( const QByteArray &newPasswordBytes, const QString &email, diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index fcb1b1064..ddcb994d6 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_lock_widgets.h" #include "window/themes/window_theme.h" //#include "platform/platform_specific.h" +#include "base/unixtime.h" #include "calls/calls_instance.h" #include "support/support_helper.h" #include "facades.h" @@ -43,6 +44,7 @@ namespace Main { namespace { constexpr auto kLegacyCallsPeerToPeerNobody = 4; +constexpr auto kTmpPasswordReserveTime = TimeId(10); [[nodiscard]] QString ValidatedInternalLinksDomain( not_null session) { @@ -155,6 +157,20 @@ Session::Session( _api->requestNotifySettings(MTP_inputNotifyBroadcasts()); } +void Session::setTmpPassword(const QByteArray &password, TimeId validUntil) { + if (_tmpPassword.isEmpty() || validUntil > _tmpPasswordValidUntil) { + _tmpPassword = password; + _tmpPasswordValidUntil = validUntil; + } +} + +QByteArray Session::validTmpPassword() const { + return (_tmpPasswordValidUntil + >= base::unixtime::now() + kTmpPasswordReserveTime) + ? _tmpPassword + : QByteArray(); +} + // Can be called only right before ~Session. void Session::finishLogout() { updates().updateOnline(); diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 712e86397..cd29bcb5b 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -145,6 +145,9 @@ public: [[nodiscard]] QString createInternalLink(const QString &query) const; [[nodiscard]] QString createInternalLinkFull(const QString &query) const; + void setTmpPassword(const QByteArray &password, TimeId validUntil); + [[nodiscard]] QByteArray validTmpPassword() const; + // Can be called only right before ~Session. void finishLogout(); @@ -190,6 +193,9 @@ private: base::flat_set> _windows; base::Timer _saveSettingsTimer; + QByteArray _tmpPassword; + TimeId _tmpPasswordValidUntil = 0; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/mtproto/mtproto_response.h b/Telegram/SourceFiles/mtproto/mtproto_response.h index 784e9b5e2..247f1c7db 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_response.h +++ b/Telegram/SourceFiles/mtproto/mtproto_response.h @@ -38,8 +38,12 @@ private: }; +inline bool IsFloodError(const QString &type) { + return type.startsWith(qstr("FLOOD_WAIT_")); +} + inline bool IsFloodError(const Error &error) { - return error.type().startsWith(qstr("FLOOD_WAIT_")); + return IsFloodError(error.type()); } inline bool IsTemporaryError(const Error &error) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 376e3b36d..ba91af348 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -16,8 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history.h" #include "data/data_user.h" // UserData::isBot. +#include "boxes/passcode_box.h" #include "core/local_url_handlers.h" // TryConvertUrlToLocal. #include "core/file_utilities.h" // File::OpenUrl. +#include "core/core_cloud_password.h" // Core::CloudPasswordState +#include "lang/lang_keys.h" #include "apiwrap.h" #include @@ -104,11 +107,19 @@ CheckoutProcess::CheckoutProcess( ) | rpl::start_with_next([=](const FormUpdate &update) { handleFormUpdate(update); }, _lifetime); + _panel->backRequests( ) | rpl::start_with_next([=] { panelCancelEdit(); }, _panel->lifetime()); showForm(); + + if (mode == Mode::Payment) { + _session->api().passwordState( + ) | rpl::start_with_next([=](const Core::CloudPasswordState &state) { + _form->setHasPassword(!!state.request); + }, _lifetime); + } } CheckoutProcess::~CheckoutProcess() { @@ -132,6 +143,9 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { if (!_initialSilentValidation) { showForm(); } + if (_form->paymentMethod().savedCredentials) { + _session->api().reloadPasswordState(); + } }, [&](const ThumbnailUpdated &data) { _panel->updateFormThumbnail(data.thumbnail); }, [&](const ValidateFinished &) { @@ -139,12 +153,19 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _initialSilentValidation = false; } showForm(); - if (_submitState == SubmitState::Validation) { - _submitState = SubmitState::Validated; + const auto submitted = (_submitState == SubmitState::Validating); + _submitState = SubmitState::Validated; + if (submitted) { panelSubmit(); } - }, [&](const PaymentMethodUpdate &) { + }, [&](const PaymentMethodUpdate &data) { showForm(); + if (data.requestNewPassword) { + requestSetPassword(); + } + }, [&](const TmpPasswordRequired &) { + _submitState = SubmitState::Validated; + requestPassword(); }, [&](const VerificationNeeded &data) { if (!_panel->showWebview(data.url, false)) { File::OpenUrl(data.url); @@ -176,7 +197,7 @@ void CheckoutProcess::handleError(const Error &error) { } break; case Error::Type::Validate: { - if (_submitState == SubmitState::Validation + if (_submitState == SubmitState::Validating || _submitState == SubmitState::Validated) { _submitState = SubmitState::None; } @@ -243,9 +264,19 @@ void CheckoutProcess::handleError(const Error &error) { showToast({ "Error: " + id }); } } break; + case Error::Type::TmpPassword: + if (const auto box = _enterPasswordBox.data()) { + if (!box->handleCustomCheckError(id)) { + showToast({ "Error: Could not generate tmp password." }); + } + } + break; case Error::Type::Send: + if (const auto box = _enterPasswordBox.data()) { + box->closeBox(); + } if (_submitState == SubmitState::Finishing) { - _submitState = SubmitState::None; + _submitState = SubmitState::Validated; } if (id == u"PAYMENT_FAILED"_q) { showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message @@ -256,6 +287,8 @@ void CheckoutProcess::handleError(const Error &error) { || id == u"PAYMENT_CREDENTIALS_INVALID"_q || id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) { showToast({ "Error: " + id + ". Your card has not been billed." }); + } else if (id == u"TMP_PASSWORD_INVALID"_q) { + // #TODO payments save } else { showToast({ "Error: " + id }); } @@ -301,7 +334,7 @@ void CheckoutProcess::panelSubmit() { if (_form->invoice().receipt.paid) { closeAndReactivate(); return; - } else if (_submitState == SubmitState::Validation + } else if (_submitState == SubmitState::Validating || _submitState == SubmitState::Finishing) { return; } @@ -310,25 +343,25 @@ void CheckoutProcess::panelSubmit() { const auto &options = _form->shippingOptions(); if (!options.list.empty() && options.selectedId.isEmpty()) { chooseShippingOption(); - return; } else if (_submitState != SubmitState::Validated && options.list.empty() && (invoice.isShippingAddressRequested || invoice.isNameRequested || invoice.isEmailRequested || invoice.isPhoneRequested)) { - _submitState = SubmitState::Validation; + _submitState = SubmitState::Validating; _form->validateInformation(_form->savedInformation()); - return; } else if (!method.newCredentials && !method.savedCredentials) { editPaymentMethod(); - return; + } else { + _submitState = SubmitState::Finishing; + _form->submit(); } - _submitState = SubmitState::Finishing; - _form->submit(); } -void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) { +void CheckoutProcess::panelWebviewMessage( + const QJsonDocument &message, + bool saveInformation) { if (!message.isArray()) { LOG(("Payments Error: " "Not an array received in buy_callback arguments.")); @@ -371,7 +404,7 @@ void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) { .data = QJsonDocument( credentials.toObject() ).toJson(QJsonDocument::Compact), - .saveOnServer = false, // #TODO payments save + .saveOnServer = saveInformation, }); }); } @@ -399,8 +432,10 @@ void CheckoutProcess::panelEditPaymentMethod() { editPaymentMethod(); } -void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) { - _form->validateCard(data); +void CheckoutProcess::panelValidateCard( + Ui::UncheckedCardDetails data, + bool saveInformation) { + _form->validateCard(data, saveInformation); } void CheckoutProcess::panelEditShippingInformation() { @@ -466,6 +501,70 @@ void CheckoutProcess::editPaymentMethod() { _panel->choosePaymentMethod(_form->paymentMethod().ui); } +void CheckoutProcess::requestSetPassword() { + _session->api().reloadPasswordState(); + _panel->askSetPassword(); +} + +void CheckoutProcess::requestPassword() { + getPasswordState([=](const Core::CloudPasswordState &state) { + auto fields = PasscodeBox::CloudFields::From(state); + fields.customTitle = tr::lng_payments_password_title(); + fields.customDescription = tr::lng_payments_password_description( + tr::now, + lt_card, + _form->paymentMethod().savedCredentials.title); + fields.customSubmitButton = tr::lng_payments_password_submit(); + fields.customCheckCallback = [=]( + const Core::CloudPasswordResult &result) { + _form->submit(result); + }; + auto owned = Box(_session, fields); + _enterPasswordBox = owned.data(); + _panel->showBox(std::move(owned)); + }); +} + +void CheckoutProcess::panelSetPassword() { + getPasswordState([=](const Core::CloudPasswordState &state) { + if (state.request) { + return; + } + auto owned = Box( + _session, + PasscodeBox::CloudFields::From(state)); + const auto box = owned.data(); + + rpl::merge( + box->newPasswordSet() | rpl::to_empty, + box->passwordReloadNeeded() + ) | rpl::start_with_next([=] { + _session->api().reloadPasswordState(); + }, box->lifetime()); + + box->clearUnconfirmedPassword( + ) | rpl::start_with_next([=] { + _session->api().clearUnconfirmedPassword(); + }, box->lifetime()); + + _panel->showBox(std::move(owned)); + }); +} + +void CheckoutProcess::getPasswordState( + Fn callback) { + Expects(callback != nullptr); + + if (_gettingPasswordState) { + return; + } + _session->api().passwordState( + ) | rpl::start_with_next([=](const Core::CloudPasswordState &state) { + _gettingPasswordState.destroy(); + callback(state); + }, _gettingPasswordState); +} + void CheckoutProcess::panelChooseShippingOption() { if (_submitState != SubmitState::None) { return; @@ -492,6 +591,9 @@ void CheckoutProcess::panelChangeTips(int64 value) { void CheckoutProcess::panelValidateInformation( Ui::RequestedInformation data) { + if (_submitState == SubmitState::Validated) { + _submitState = SubmitState::None; + } _form->validateInformation(data); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 06ca7ca7e..26d207d85 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -11,11 +11,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" class HistoryItem; +class PasscodeBox; + +namespace Core { +class CloudPasswordState; +} // namespace Core namespace Main { class Session; } // namespace Main +namespace Ui { +class GenericBox; +} // namespace Ui + namespace Payments::Ui { class Panel; enum class InformationField; @@ -55,7 +64,7 @@ public: private: enum class SubmitState { None, - Validation, + Validating, Validated, Finishing, }; @@ -77,13 +86,22 @@ private: void chooseTips(); void editPaymentMethod(); + void requestSetPassword(); + void requestSetPasswordSure(QPointer old); + void requestPassword(); + void getPasswordState( + Fn callback); + void performInitialSilentValidation(); void panelRequestClose() override; void panelCloseSure() override; void panelSubmit() override; - void panelWebviewMessage(const QJsonDocument &message) override; + void panelWebviewMessage( + const QJsonDocument &message, + bool saveInformation) override; bool panelWebviewNavigationAttempt(const QString &uri) override; + void panelSetPassword() override; void panelCancelEdit() override; void panelEditPaymentMethod() override; @@ -97,7 +115,9 @@ private: void panelChangeTips(int64 value) override; void panelValidateInformation(Ui::RequestedInformation data) override; - void panelValidateCard(Ui::UncheckedCardDetails data) override; + void panelValidateCard( + Ui::UncheckedCardDetails data, + bool saveInformation) override; void panelShowBox(object_ptr box) override; QString panelWebviewDataPath() override; @@ -105,10 +125,12 @@ private: const not_null _session; const std::unique_ptr _form; const std::unique_ptr _panel; + QPointer _enterPasswordBox; Fn _reactivate; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; + rpl::lifetime _gettingPasswordState; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 63597878e..c1b001af1 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "stripe/stripe_card_validator.h" #include "ui/image/image.h" #include "apiwrap.h" +#include "core/core_cloud_password.h" #include "styles/style_payments.h" // paymentsThumbnailSize. #include @@ -32,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Payments { namespace { +constexpr auto kPasswordPeriod = 15 * TimeId(60); + [[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) { return address.match([](const MTPDpostAddress &data) { return Ui::Address{ @@ -381,11 +384,10 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { void Form::processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data) { - // #TODO payments save - //_nativePayment.savedCredentials = SavedCredentials{ - // .id = qs(data.vid()), - // .title = qs(data.vtitle()), - //}; + _paymentMethod.savedCredentials = SavedCredentials{ + .id = qs(data.vid()), + .title = qs(data.vtitle()), + }; refreshPaymentMethodDetails(); } @@ -395,6 +397,9 @@ void Form::refreshPaymentMethodDetails() { _paymentMethod.ui.title = entered ? entered.title : saved.title; _paymentMethod.ui.ready = entered || saved; _paymentMethod.ui.native.defaultCountry = defaultCountry(); + _paymentMethod.ui.canSaveInformation + = _paymentMethod.ui.native.canSaveInformation + = _details.canSaveCredentials || _details.passwordMissing; } QString Form::defaultPhone() const { @@ -452,7 +457,16 @@ void Form::fillStripeNativeMethod() { } void Form::submit() { - Expects(!_paymentMethod.newCredentials.data.isEmpty()); // #TODO payments save + Expects(_paymentMethod.newCredentials + || _paymentMethod.savedCredentials); + + const auto password = _paymentMethod.newCredentials + ? QByteArray() + : _session->validTmpPassword(); + if (!_paymentMethod.newCredentials && password.isEmpty()) { + _updates.fire(TmpPasswordRequired{}); + return; + } using Flag = MTPpayments_SendPaymentForm::Flag; _api.request(MTPpayments_SendPaymentForm( @@ -468,9 +482,16 @@ void Form::submit() { MTP_int(_msgId), MTP_string(_requestedInformationId), MTP_string(_shippingOptions.selectedId), - MTP_inputPaymentCredentials( - MTP_flags(0), - MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))), + (_paymentMethod.newCredentials + ? MTP_inputPaymentCredentials( + MTP_flags((_paymentMethod.newCredentials.saveOnServer + && _details.canSaveCredentials) + ? MTPDinputPaymentCredentials::Flag::f_save + : MTPDinputPaymentCredentials::Flag(0)), + MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))) + : MTP_inputPaymentCredentialsSaved( + MTP_string(_paymentMethod.savedCredentials.id), + MTP_bytes(password))), MTP_long(_invoice.tipsSelected) )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { @@ -483,6 +504,27 @@ void Form::submit() { }).send(); } +void Form::submit(const Core::CloudPasswordResult &result) { + if (_passwordRequestId) { + return; + } + _passwordRequestId = _api.request(MTPaccount_GetTmpPassword( + result.result, + MTP_int(kPasswordPeriod) + )).done([=](const MTPaccount_TmpPassword &result) { + _passwordRequestId = 0; + result.match([&](const MTPDaccount_tmpPassword &data) { + _session->setTmpPassword( + data.vtmp_password().v, + data.vvalid_until().v); + submit(); + }); + }).fail([=](const MTP::Error &error) { + _passwordRequestId = 0; + _updates.fire(Error{ Error::Type::TmpPassword, error.type() }); + }).send(); +} + void Form::validateInformation(const Ui::RequestedInformation &information) { if (_validateRequestId) { if (_validatedInformation == information) { @@ -573,7 +615,9 @@ Error Form::informationErrorLocal( return Error(); } -void Form::validateCard(const Ui::UncheckedCardDetails &details) { +void Form::validateCard( + const Ui::UncheckedCardDetails &details, + bool saveInformation) { Expects(!v::is_null(_paymentMethod.native.data)); if (!validateCardLocal(details)) { @@ -581,7 +625,7 @@ void Form::validateCard(const Ui::UncheckedCardDetails &details) { } const auto &native = _paymentMethod.native.data; if (const auto stripe = std::get_if(&native)) { - validateCard(*stripe, details); + validateCard(*stripe, details, saveInformation); } else { Unexpected("Native payment provider in Form::validateCard."); } @@ -635,7 +679,8 @@ Error Form::cardErrorLocal(const Ui::UncheckedCardDetails &details) const { void Form::validateCard( const StripePaymentMethod &method, - const Ui::UncheckedCardDetails &details) { + const Ui::UncheckedCardDetails &details, + bool saveInformation) { Expects(!method.publishableKey.isEmpty()); if (_stripe) { @@ -673,7 +718,7 @@ void Form::validateCard( { "type", "card" }, { "id", token.tokenId() }, }).toJson(QJsonDocument::Compact), - .saveOnServer = false, // #TODO payments save + .saveOnServer = saveInformation, }); } })); @@ -683,8 +728,17 @@ void Form::setPaymentCredentials(const NewCredentials &credentials) { Expects(!credentials.empty()); _paymentMethod.newCredentials = credentials; + const auto requestNewPassword = credentials.saveOnServer + && !_details.canSaveCredentials + && _details.passwordMissing; refreshPaymentMethodDetails(); - _updates.fire(PaymentMethodUpdate{}); + _updates.fire(PaymentMethodUpdate{ requestNewPassword }); +} + +void Form::setHasPassword(bool has) { + if (_details.passwordMissing) { + _details.canSaveCredentials = has; + } } void Form::setShippingOption(const QString &id) { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index ae37feece..2edf2acb7 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -13,6 +13,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class Image; +namespace Core { +struct CloudPasswordResult; +} // namespace Core + namespace Stripe { class APIClient; } // namespace Stripe @@ -107,10 +111,13 @@ struct ThumbnailUpdated { QImage thumbnail; }; struct ValidateFinished {}; -struct PaymentMethodUpdate {}; +struct PaymentMethodUpdate { + bool requestNewPassword = false; +}; struct VerificationNeeded { QString url; }; +struct TmpPasswordRequired {}; struct PaymentFinished { MTPUpdates updates; }; @@ -120,6 +127,7 @@ struct Error { Form, Validate, Stripe, + TmpPassword, Send, }; Type type = Type::None; @@ -139,6 +147,7 @@ struct FormUpdate : std::variant< ValidateFinished, PaymentMethodUpdate, VerificationNeeded, + TmpPasswordRequired, PaymentFinished, Error> { using variant::variant; @@ -170,11 +179,15 @@ public: } void validateInformation(const Ui::RequestedInformation &information); - void validateCard(const Ui::UncheckedCardDetails &details); + void validateCard( + const Ui::UncheckedCardDetails &details, + bool saveInformation); void setPaymentCredentials(const NewCredentials &credentials); + void setHasPassword(bool has); void setShippingOption(const QString &id); void setTips(int64 value); void submit(); + void submit(const Core::CloudPasswordResult &result); private: void fillInvoiceFromMessage(); @@ -208,7 +221,8 @@ private: void validateCard( const StripePaymentMethod &method, - const Ui::UncheckedCardDetails &details); + const Ui::UncheckedCardDetails &details, + bool saveInformation); bool validateInformationLocal( const Ui::RequestedInformation &information) const; @@ -235,6 +249,7 @@ private: Ui::RequestedInformation _validatedInformation; mtpRequestId _validateRequestId = 0; + mtpRequestId _passwordRequestId = 0; std::unique_ptr _stripe; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 4abdf760d..d084cc1bd 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/widgets/checkbox.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "lang/lang_keys.h" @@ -246,7 +247,7 @@ void EditCard::setupControls() { const auto inner = setupContent(); _submit->addClickHandler([=] { - _delegate->panelValidateCard(collect()); + _delegate->panelValidateCard(collect(), (_save && _save->checked())); }); _cancel->addClickHandler([=] { _delegate->panelCancelEdit(); @@ -357,6 +358,14 @@ not_null EditCard::setupContent() { }, lifetime()); } } + if (_native.canSaveInformation) { + _save = inner->add( + object_ptr( + inner, + tr::lng_payments_save_information(tr::now), + false), + st::paymentsSaveCheckboxPadding); + } return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h index da6a23c4c..ae4cce54b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -15,6 +15,7 @@ namespace Ui { class ScrollArea; class FadeShadow; class RoundButton; +class Checkbox; } // namespace Ui namespace Payments::Ui { @@ -62,6 +63,7 @@ private: std::unique_ptr _name; std::unique_ptr _country; std::unique_ptr _zip; + Checkbox *_save = nullptr; CardField _focusField = CardField::Number; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 48a19dea1..cf74d02cc 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -229,6 +229,18 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { showEditCard(method.native, CardField::Number); } else if (!showWebview(method.url, true)) { // #TODO payments errors not supported + } else if (method.canSaveInformation) { + const auto &padding = st::paymentsPanelPadding; + _saveWebviewInformation = CreateChild( + _webviewBottom.get(), + tr::lng_payments_save_information(tr::now), + false); + const auto height = padding.top() + + _saveWebviewInformation->heightNoMargins() + + padding.bottom(); + _saveWebviewInformation->moveToLeft(padding.right(), padding.top()); + _saveWebviewInformation->show(); + _webviewBottom->resize(_webviewBottom->width(), height); } } @@ -244,7 +256,17 @@ bool Panel::showWebview(const QString &url, bool allowBack) { bool Panel::createWebview() { auto container = base::make_unique_q(_widget.get()); - container->setGeometry(_widget->innerGeometry()); + _webviewBottom = std::make_unique(_widget.get()); + const auto bottom = _webviewBottom.get(); + bottom->show(); + + bottom->heightValue( + ) | rpl::start_with_next([=, raw = container.get()](int height) { + const auto inner = _widget->innerGeometry(); + bottom->move(inner.x(), inner.y() + inner.height() - height); + raw->resize(inner.width(), inner.height() - height); + bottom->resizeToWidth(inner.width()); + }, bottom->lifetime()); container->show(); _webview = std::make_unique( @@ -257,6 +279,9 @@ bool Panel::createWebview() { if (_webview.get() == raw) { _webview = nullptr; } + if (_webviewBottom.get() == bottom) { + _webviewBottom = nullptr; + } }); if (!raw->widget()) { return false; @@ -268,7 +293,9 @@ bool Panel::createWebview() { }, container->lifetime()); raw->setMessageHandler([=](const QJsonDocument &message) { - _delegate->panelWebviewMessage(message); + const auto save = _saveWebviewInformation + && _saveWebviewInformation->checked(); + _delegate->panelWebviewMessage(message, save); }); raw->setNavigationHandler([=](const QString &uri) { @@ -308,6 +335,22 @@ void Panel::choosePaymentMethod(const PaymentMethodDetails &method) { })); } +void Panel::askSetPassword() { + showBox(Box([=](not_null box) { + box->addRow( + object_ptr( + box.get(), + tr::lng_payments_need_password(), + st::boxLabel), + st::boxPadding); + box->addButton(tr::lng_continue(), [=] { + _delegate->panelSetPassword(); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +} + void Panel::showEditCard( const NativeMethodDetails &native, CardField field) { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 6d39159d3..488419a54 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -10,8 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" namespace Ui { +class RpWidget; class SeparatePanel; class BoxContent; +class Checkbox; } // namespace Ui namespace Webview { @@ -65,6 +67,7 @@ public: void chooseShippingOption(const ShippingOptions &options); void chooseTips(const Invoice &invoice); void choosePaymentMethod(const PaymentMethodDetails &method); + void askSetPassword(); bool showWebview(const QString &url, bool allowBack); @@ -81,6 +84,8 @@ private: const not_null _delegate; std::unique_ptr _widget; std::unique_ptr _webview; + std::unique_ptr _webviewBottom; + QPointer _saveWebviewInformation; QPointer _weakFormSummary; QPointer _weakEditInformation; QPointer _weakEditCard; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index ce17f7c61..f833703c0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -154,6 +154,7 @@ struct NativeMethodDetails { bool needCountry = false; bool needZip = false; bool needCardholderName = false; + bool canSaveInformation = false; }; struct PaymentMethodDetails { @@ -161,6 +162,7 @@ struct PaymentMethodDetails { NativeMethodDetails native; QString url; bool ready = false; + bool canSaveInformation = false; }; enum class CardField { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index cce968fe1..13a9a86c6 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -28,8 +28,11 @@ public: virtual void panelRequestClose() = 0; virtual void panelCloseSure() = 0; virtual void panelSubmit() = 0; - virtual void panelWebviewMessage(const QJsonDocument &message) = 0; + virtual void panelWebviewMessage( + const QJsonDocument &message, + bool saveInformation) = 0; virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; + virtual void panelSetPassword() = 0; virtual void panelCancelEdit() = 0; virtual void panelEditPaymentMethod() = 0; @@ -43,7 +46,9 @@ public: virtual void panelChangeTips(int64 value) = 0; virtual void panelValidateInformation(RequestedInformation data) = 0; - virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0; + virtual void panelValidateCard( + Ui::UncheckedCardDetails data, + bool saveInformation) = 0; virtual void panelShowBox(object_ptr box) = 0; virtual QString panelWebviewDataPath() = 0; From 889e0dc0356bd5a704977ffdcc2c804168c5b871 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 21:30:42 +0400 Subject: [PATCH 036/127] Fix build for macOS / Linux. --- Telegram/SourceFiles/payments/ui/payments_field.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index 351b1a35e..e9b9202be 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -47,7 +47,7 @@ struct FieldValidateResult { bool finished = false; }; -[[nodiscard]] auto RangeLengthValidator(int minLength, int maxLength) { +[[nodiscard]] inline auto RangeLengthValidator(int minLength, int maxLength) { return [=](FieldValidateRequest request) { return FieldValidateResult{ .value = request.nowValue, @@ -58,15 +58,15 @@ struct FieldValidateResult { }; } -[[nodiscard]] auto MaxLengthValidator(int maxLength) { +[[nodiscard]] inline auto MaxLengthValidator(int maxLength) { return RangeLengthValidator(0, maxLength); } -[[nodiscard]] auto RequiredValidator() { +[[nodiscard]] inline auto RequiredValidator() { return RangeLengthValidator(1, std::numeric_limits::max()); } -[[nodiscard]] auto RequiredFinishedValidator() { +[[nodiscard]] inline auto RequiredFinishedValidator() { return [=](FieldValidateRequest request) { return FieldValidateResult{ .value = request.nowValue, From e6ba6050e7f88fe3b3dea50f7d738655aa94c3c4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 22:19:00 +0400 Subject: [PATCH 037/127] Update button on paid invoices to 'Receipt'. --- .../SourceFiles/data/data_media_types.cpp | 36 ++++++++++++------- Telegram/SourceFiles/data/data_media_types.h | 4 +++ Telegram/SourceFiles/history/history_item.h | 2 ++ .../SourceFiles/history/history_message.cpp | 15 ++++++-- .../SourceFiles/history/history_message.h | 1 + .../SourceFiles/history/history_service.cpp | 4 +++ 6 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index bccad1a5e..5fa3c9273 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -80,19 +80,20 @@ constexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60); [[nodiscard]] Invoice ComputeInvoiceData( not_null item, const MTPDmessageMediaInvoice &data) { - auto result = Invoice(); - result.isTest = data.is_test(); - result.amount = data.vtotal_amount().v; - result.currency = qs(data.vcurrency()); - result.description = qs(data.vdescription()); - result.title = TextUtilities::SingleLine(qs(data.vtitle())); - result.receiptMsgId = data.vreceipt_msg_id().value_or_empty(); - if (const auto photo = data.vphoto()) { - result.photo = item->history()->owner().photoFromWeb( - *photo, - ImageLocation()); - } - return result; + return { + .receiptMsgId = data.vreceipt_msg_id().value_or_empty(), + .amount = data.vtotal_amount().v, + .currency = qs(data.vcurrency()), + .title = TextUtilities::SingleLine(qs(data.vtitle())), + .description = qs(data.vdescription()), + .photo = (data.vphoto() + ? item->history()->owner().photoFromWeb( + *data.vphoto(), + ImageLocation()) + : nullptr), + .isMultipleAllowed = item->history()->isChannel(), // #TODO payments + .isTest = data.is_test(), + }; } [[nodiscard]] QString WithCaptionDialogsText( @@ -188,6 +189,10 @@ PollData *Media::poll() const { return nullptr; } +void Media::setInvoiceReceiptId(MsgId id) { + Unexpected("Media::setInvoiceReceiptId."); +} + bool Media::uploading() const { return false; } @@ -1190,6 +1195,11 @@ const Invoice *MediaInvoice::invoice() const { return &_invoice; } +void MediaInvoice::setInvoiceReceiptId(MsgId id) { + _invoice.receiptMsgId = id; + parent()->checkBuyButton(); +} + bool MediaInvoice::hasReplyPreview() const { if (const auto photo = _invoice.photo) { return !photo->isNull(); diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index b99cdd065..edf719f9e 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -62,6 +62,7 @@ struct Invoice { QString title; QString description; PhotoData *photo = nullptr; + bool isMultipleAllowed = false; bool isTest = false; }; @@ -84,6 +85,8 @@ public: virtual Data::CloudImage *location() const; virtual PollData *poll() const; + virtual void setInvoiceReceiptId(MsgId id); + virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; virtual bool canBeGrouped() const; @@ -381,6 +384,7 @@ public: std::unique_ptr clone(not_null parent) override; const Invoice *invoice() const override; + void setInvoiceReceiptId(MsgId id) override; bool hasReplyPreview() const override; Image *replyPreview() const override; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 960bb0f1d..b9517f2ae 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -76,6 +76,8 @@ public: virtual MsgId dependencyMsgId() const { return 0; } + virtual void checkBuyButton() { + } [[nodiscard]] virtual bool notificationReady() const { return true; } diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index c79bc2ae6..3165d328b 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -1215,6 +1215,10 @@ void HistoryMessage::returnSavedMedia() { void HistoryMessage::setMedia(const MTPMessageMedia &media) { _media = CreateMedia(this, media); + checkBuyButton(); +} + +void HistoryMessage::checkBuyButton() { if (const auto invoice = _media ? _media->invoice() : nullptr) { if (invoice->receiptMsgId) { replaceBuyWithReceiptInMarkup(); @@ -1341,11 +1345,18 @@ std::unique_ptr HistoryMessage::CreateMedia( } void HistoryMessage::replaceBuyWithReceiptInMarkup() { - if (auto markup = inlineReplyMarkup()) { + if (const auto markup = inlineReplyMarkup()) { for (auto &row : markup->rows) { for (auto &button : row) { if (button.type == HistoryMessageMarkupButton::Type::Buy) { - button.text = tr::lng_payments_receipt_button(tr::now); + const auto receipt = tr::lng_payments_receipt_button(tr::now); + if (button.text != receipt) { + button.text = receipt; + if (markup->inlineKeyboard) { + markup->inlineKeyboard = nullptr; + history()->owner().requestItemResize(this); + } + } } } } diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index 36e3ac653..7959087c3 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -118,6 +118,7 @@ public: void refreshSentMedia(const MTPMessageMedia *media); void returnSavedMedia() override; void setMedia(const MTPMessageMedia &media); + void checkBuyButton() override; [[nodiscard]] static std::unique_ptr CreateMedia( not_null item, const MTPMessageMedia &media); diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 132b3e3c8..03912ae23 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -765,6 +765,10 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() { if (payment->msg) { if (const auto media = payment->msg->media()) { if (const auto invoice = media->invoice()) { + if (!invoice->isMultipleAllowed + && !invoice->receiptMsgId) { + media->setInvoiceReceiptId(id); + } return textcmdLink(1, invoice->title); } } From 0188719d0426e4768d8a09b582b5442d522bf84c Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 09:05:02 +0400 Subject: [PATCH 038/127] Fix payments with zero tips. --- Telegram/SourceFiles/payments/payments_checkout_process.cpp | 6 ++++-- Telegram/SourceFiles/payments/payments_form.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index ba91af348..1a60cf4b8 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -566,7 +566,8 @@ void CheckoutProcess::getPasswordState( } void CheckoutProcess::panelChooseShippingOption() { - if (_submitState != SubmitState::None) { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { return; } chooseShippingOption(); @@ -578,7 +579,8 @@ void CheckoutProcess::panelChangeShippingOption(const QString &id) { } void CheckoutProcess::panelChooseTips() { - if (_submitState != SubmitState::None) { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { return; } chooseTips(); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index c1b001af1..d6cf8976f 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -476,7 +476,7 @@ void Form::submit() { | (_shippingOptions.selectedId.isEmpty() ? Flag(0) : Flag::f_shipping_option_id) - | (_invoice.tipsSelected ? Flag::f_tip_amount : Flag(0))), + | (_invoice.tipsMax > 0 ? Flag::f_tip_amount : Flag(0))), MTP_long(_details.formId), _peer->input, MTP_int(_msgId), From 1cc1f380d0424b279058d33006e559f6092e4a18 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 13:27:39 +0400 Subject: [PATCH 039/127] Implement a nice money input field. --- Telegram/Resources/langs/lang.strings | 7 +- .../payments/payments_checkout_process.cpp | 11 +- .../SourceFiles/payments/ui/payments.style | 10 + .../payments/ui/payments_field.cpp | 314 +++++++++++++++++- .../SourceFiles/payments/ui/payments_field.h | 4 +- .../payments/ui/payments_form_summary.cpp | 4 +- .../payments/ui/payments_panel.cpp | 54 +-- .../SourceFiles/ui/text/format_values.cpp | 229 ++++++------- Telegram/SourceFiles/ui/text/format_values.h | 16 + 9 files changed, 481 insertions(+), 168 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7b7241944..9a59fa5c2 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1891,9 +1891,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_password_title" = "Payment Confirmation"; "lng_payments_password_description" = "Your card {card} is on file. To pay with this card, please enter your 2-Step-Verification password."; "lng_payments_password_submit" = "Pay"; -"lng_payments_tips_label" = "Tips"; -"lng_payments_tips_title" = "Tips"; -"lng_payments_tips_enter" = "Enter tips amount"; +"lng_payments_tips_label" = "Tip (Optional)"; +"lng_payments_tips_add" = "Add Tip"; +"lng_payments_tips_box_title" = "Add Tip"; +"lng_payments_tips_max" = "Max possible tip amount: {amount}"; "lng_call_status_incoming" = "is calling you..."; "lng_call_status_connecting" = "connecting..."; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 1a60cf4b8..290b7762c 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -418,7 +418,8 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { } void CheckoutProcess::panelCancelEdit() { - if (_submitState != SubmitState::None) { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { return; } showForm(); @@ -463,7 +464,8 @@ void CheckoutProcess::showForm() { } void CheckoutProcess::showEditInformation(Ui::InformationField field) { - if (_submitState != SubmitState::None) { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { return; } _panel->showEditInformation( @@ -473,6 +475,8 @@ void CheckoutProcess::showEditInformation(Ui::InformationField field) { } void CheckoutProcess::showInformationError(Ui::InformationField field) { + Expects(_submitState != SubmitState::Validated); + if (_submitState != SubmitState::None) { return; } @@ -483,7 +487,8 @@ void CheckoutProcess::showInformationError(Ui::InformationField field) { } void CheckoutProcess::showCardError(Ui::CardField field) { - if (_submitState != SubmitState::None) { + if (_submitState != SubmitState::None + && _submitState != SubmitState::Validated) { return; } _panel->showCardError(_form->paymentMethod().ui.native, field); diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 3dbc9eeaa..5a7ba8214 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -59,6 +59,10 @@ paymentsIconPhone: icon {{ "payments/payment_phone", menuIconFg }}; paymentsIconShippingMethod: icon {{ "payments/payment_shipping", menuIconFg }}; paymentsField: defaultInputField; +paymentsFieldAdditional: FlatLabel(defaultFlatLabel) { + style: boxTextStyle; +} + paymentsFieldPadding: margins(28px, 0px, 28px, 2px); paymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px); paymentsExpireCvcSkip: 34px; @@ -79,3 +83,9 @@ paymentsShippingPrice: FlatLabel(defaultFlatLabel) { } paymentsShippingLabelPosition: point(43px, 8px); paymentsShippingPricePosition: point(43px, 29px); + +paymentTipsErrorLabel: FlatLabel(defaultFlatLabel) { + minWidth: 275px; + textFg: boxTextFgError; +} +paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px); diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 20dc96160..287d6046b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/boxes/country_select_box.h" +#include "ui/text/format_values.h" #include "ui/ui_utility.h" #include "ui/special_fields.h" #include "data/data_countries.h" @@ -16,12 +17,190 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "styles/style_payments.h" +#include + namespace Payments::Ui { namespace { +struct SimpleFieldState { + QString value; + int position = 0; +}; + +[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) { + return (rule.thousands == '.' || rule.thousands == ',') + ? ' ' + : rule.thousands; +} + +[[nodiscard]] QString RemoveNonNumbers(QString value) { + return value.replace(QRegularExpression("[^0-9]"), QString()); +} + +[[nodiscard]] SimpleFieldState CleanMoneyState( + const CurrencyRule &rule, + SimpleFieldState state) { + const auto withDecimal = state.value.replace( + QChar('.'), + rule.decimal + ).replace( + QChar(','), + rule.decimal + ); + const auto digitsLimit = 16 - rule.exponent; + const auto beforePosition = state.value.mid(0, state.position); + auto decimalPosition = withDecimal.lastIndexOf(rule.decimal); + if (decimalPosition < 0) { + state = { + .value = RemoveNonNumbers(state.value), + .position = RemoveNonNumbers(beforePosition).size(), + }; + } else { + const auto onlyNumbersBeforeDecimal = RemoveNonNumbers( + state.value.mid(0, decimalPosition)); + state = { + .value = (onlyNumbersBeforeDecimal + + QChar(rule.decimal) + + RemoveNonNumbers(state.value.mid(decimalPosition + 1))), + .position = (RemoveNonNumbers(beforePosition).size() + + (state.position > decimalPosition ? 1 : 0)), + }; + decimalPosition = onlyNumbersBeforeDecimal.size(); + const auto maxLength = decimalPosition + 1 + rule.exponent; + if (state.value.size() > maxLength) { + state = { + .value = state.value.mid(0, maxLength), + .position = std::min(state.position, maxLength), + }; + } + } + if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) { + state = { + .value = QChar('0') + state.value, + .position = state.position + 1, + }; + if (decimalPosition >= 0) { + ++decimalPosition; + } + } + auto skip = 0; + while (state.value.size() > skip + 1 + && state.value[skip] == QChar('0') + && state.value[skip + 1] != QChar(rule.decimal)) { + ++skip; + } + state = { + .value = state.value.mid(skip), + .position = std::max(state.position - skip, 0), + }; + if (decimalPosition >= 0) { + Assert(decimalPosition >= skip); + decimalPosition -= skip; + } + if (decimalPosition > digitsLimit) { + state = { + .value = (state.value.mid(0, digitsLimit) + + state.value.mid(decimalPosition)), + .position = (state.position > digitsLimit + ? std::max( + state.position - (decimalPosition - digitsLimit), + digitsLimit) + : state.position), + }; + } + return state; +} + +[[nodiscard]] SimpleFieldState PostprocessMoneyResult( + const CurrencyRule &rule, + SimpleFieldState result) { + const auto position = result.value.indexOf(rule.decimal); + const auto from = (position >= 0) ? position : result.value.size(); + for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) { + result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule))); + if (result.position >= insertAt) { + ++result.position; + } + } + return result; +} + +[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) { + return (request.wasAnchor == request.wasPosition) + && (request.wasPosition == request.nowPosition + 1) + && (request.wasValue.midRef(0, request.wasPosition - 1) + == request.nowValue.midRef(0, request.nowPosition)) + && (request.wasValue.midRef(request.wasPosition) + == request.nowValue.midRef(request.nowPosition)); +} + +[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) { + return (request.wasAnchor == request.wasPosition) + && (request.wasPosition == request.nowPosition) + && (request.wasValue.midRef(0, request.wasPosition) + == request.nowValue.midRef(0, request.nowPosition)) + && (request.wasValue.midRef(request.wasPosition + 1) + == request.nowValue.midRef(request.nowPosition)); +} + +[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) { + return [=](FieldValidateRequest request) { + const auto realNowState = [&] { + const auto backspaced = IsBackspace(request); + const auto deleted = IsDelete(request); + if (!backspaced && !deleted) { + return CleanMoneyState(rule, { + .value = request.nowValue, + .position = request.nowPosition, + }); + } + const auto realWasState = CleanMoneyState(rule, { + .value = request.wasValue, + .position = request.wasPosition, + }); + const auto changedValue = deleted + ? (realWasState.value.mid(0, realWasState.position) + + realWasState.value.mid(realWasState.position + 1)) + : (realWasState.position > 1) + ? (realWasState.value.mid(0, realWasState.position - 1) + + realWasState.value.mid(realWasState.position)) + : realWasState.value.mid(realWasState.position); + return SimpleFieldState{ + .value = changedValue, + .position = (deleted + ? realWasState.position + : std::max(realWasState.position - 1, 0)) + }; + }(); + const auto postprocessed = PostprocessMoneyResult( + rule, + realNowState); + return FieldValidateResult{ + .value = postprocessed.value, + .position = postprocessed.position, + }; + }; +} + [[nodiscard]] QString Parse(const FieldConfig &config) { if (config.type == FieldType::Country) { return Data::CountryNameByISO2(config.value); + } else if (config.type == FieldType::Money) { + const auto amount = config.value.toLongLong(); + if (!amount) { + return QString(); + } + const auto rule = LookupCurrencyRule(config.currency); + const auto value = std::abs(amount) / std::pow(10., rule.exponent); + const auto precision = (!rule.stripDotZero + || std::floor(value) != value) + ? rule.exponent + : 0; + return FormatWithSeparators( + value, + precision, + rule.decimal, + FieldThousandsSeparator(rule)); } return config.value; } @@ -32,6 +211,20 @@ namespace { const QString &countryIso2) { if (config.type == FieldType::Country) { return countryIso2; + } else if (config.type == FieldType::Money) { + const auto rule = LookupCurrencyRule(config.currency); + const auto real = QString(parsed).replace( + QChar(rule.decimal), + QChar('.') + ).replace( + QChar(','), + QChar('.') + ).replace( + QRegularExpression("[^0-9\\.]"), + QString() + ).toDouble(); + return QString::number( + int64(std::round(real * std::pow(10., rule.exponent)))); } return parsed; } @@ -46,7 +239,7 @@ namespace { case FieldType::CardCVC: case FieldType::Country: case FieldType::Phone: - case FieldType::PriceAmount: + case FieldType::Money: return true; } Unexpected("FieldType in Payments::Ui::UseMaskedField."); @@ -68,7 +261,7 @@ namespace { case FieldType::CardCVC: case FieldType::Country: case FieldType::Phone: - case FieldType::PriceAmount: + case FieldType::Money: return base::make_unique_q(parent); } Unexpected("FieldType in Payments::Ui::CreateWrap."); @@ -82,9 +275,106 @@ namespace { : static_cast(wrap.get()); } +[[nodiscard]] MaskedInputField *CreateMoneyField( + not_null wrap, + FieldConfig &config, + rpl::producer<> textPossiblyChanged) { + struct State { + CurrencyRule rule; + style::InputField st; + QString currencyText; + int currencySkip = 0; + FlatLabel *left = nullptr; + FlatLabel *right = nullptr; + }; + const auto state = wrap->lifetime().make_state(State{ + .rule = LookupCurrencyRule(config.currency), + .st = st::paymentsField, + }); + const auto &rule = state->rule; + state->currencySkip = rule.space ? state->st.font->spacew : 0; + state->currencyText = ((!rule.left && rule.space) + ? QString(QChar(' ')) + : QString()) + (*rule.international + ? QString(rule.international) + : config.currency) + ((rule.left && rule.space) + ? QString(QChar(' ')) + : QString()); + if (rule.left) { + state->left = CreateChild( + wrap.get(), + state->currencyText, + st::paymentsFieldAdditional); + } + state->right = CreateChild( + wrap.get(), + QString(), + st::paymentsFieldAdditional); + const auto leftSkip = state->left + ? (state->left->naturalWidth() + state->currencySkip) + : 0; + const auto rightSkip = st::paymentsFieldAdditional.style.font->width( + QString(QChar(rule.decimal)) + + QString(QChar('0')).repeated(rule.exponent) + + (rule.left ? QString() : state->currencyText)); + state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0); + state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0); + const auto result = CreateChild( + wrap.get(), + state->st, + std::move(config.placeholder), + Parse(config)); + result->setPlaceholderHidden(true); + if (state->left) { + state->left->move(0, state->st.textMargins.top()); + } + const auto updateRight = [=] { + const auto text = result->getLastText(); + const auto width = state->st.font->width(text); + const auto rect = result->getTextRect(); + const auto &rule = state->rule; + const auto symbol = QChar(rule.decimal); + const auto decimal = text.indexOf(symbol); + const auto zeros = (decimal >= 0) + ? std::max(rule.exponent - (text.size() - decimal - 1), 0) + : rule.stripDotZero + ? 0 + : rule.exponent; + const auto valueDecimalSeparator = (decimal >= 0 || !zeros) + ? QString() + : QString(symbol); + const auto zeroString = QString(QChar('0')); + const auto valueRightPart = (text.isEmpty() ? zeroString : QString()) + + valueDecimalSeparator + + zeroString.repeated(zeros); + const auto right = valueRightPart + + (rule.left ? QString() : state->currencyText); + state->right->setText(right); + state->right->setTextColorOverride(valueRightPart.isEmpty() + ? std::nullopt + : std::make_optional(st::windowSubTextFg->c)); + state->right->move( + (state->st.textMargins.left() + + width + + ((rule.left || !valueRightPart.isEmpty()) + ? 0 + : state->currencySkip)), + state->st.textMargins.top()); + }; + std::move( + textPossiblyChanged + ) | rpl::start_with_next(updateRight, result->lifetime()); + if (state->left) { + state->left->raise(); + } + state->right->raise(); + return result; +} + [[nodiscard]] MaskedInputField *LookupMaskedField( not_null wrap, - FieldConfig &config) { + FieldConfig &config, + rpl::producer<> textPossiblyChanged) { if (!UseMaskedField(config.type)) { return nullptr; } @@ -96,7 +386,6 @@ namespace { case FieldType::CardExpireDate: case FieldType::CardCVC: case FieldType::Country: - case FieldType::PriceAmount: return CreateChild( wrap.get(), st::paymentsField, @@ -109,6 +398,11 @@ namespace { std::move(config.placeholder), ExtractPhonePrefix(config.defaultPhone), Parse(config)); + case FieldType::Money: + return CreateMoneyField( + wrap, + config, + std::move(textPossiblyChanged)); } Unexpected("FieldType in Payments::Ui::LookupMaskedField."); } @@ -119,7 +413,10 @@ Field::Field(QWidget *parent, FieldConfig &&config) : _config(config) , _wrap(CreateWrap(parent, config)) , _input(LookupInputField(_wrap.get(), config)) -, _masked(LookupMaskedField(_wrap.get(), config)) +, _masked(LookupMaskedField( + _wrap.get(), + config, + _textPossiblyChanged.events_starting_with({}))) , _countryIso2(config.value) { if (_masked) { setupMaskedGeometry(); @@ -129,6 +426,8 @@ Field::Field(QWidget *parent, FieldConfig &&config) } if (const auto &validator = config.validator) { setupValidator(validator); + } else if (config.type == FieldType::Money) { + setupValidator(MoneyValidator(LookupCurrencyRule(config.currency))); } setupFrontBackspace(); } @@ -210,7 +509,7 @@ void Field::setupValidator(Fn validator) { const auto selectionStart = _masked->selectionStart(); const auto selectionEnd = _masked->selectionEnd(); return { - .value = value(), + .value = _masked->getLastText(), .position = position, .anchor = (selectionStart == selectionEnd ? position @@ -221,7 +520,7 @@ void Field::setupValidator(Fn validator) { } const auto cursor = _input->textCursor(); return { - .value = value(), + .value = _input->getLastText(), .position = cursor.position(), .anchor = cursor.anchor(), }; @@ -253,6 +552,7 @@ void Field::setupValidator(Fn validator) { const auto guard = gsl::finally([&] { _validating = false; save(); + _textPossiblyChanged.fire({}); }); const auto now = state(); diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index e9b9202be..5c9c37109 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -29,7 +29,7 @@ enum class FieldType { Country, Phone, Email, - PriceAmount, + Money, }; struct FieldValidateRequest { @@ -83,6 +83,7 @@ struct FieldConfig { QString value; Fn validator; Fn)> showBox; + QString currency; QString defaultPhone; QString defaultCountry; }; @@ -124,6 +125,7 @@ private: const base::unique_qptr _wrap; rpl::event_stream<> _frontBackspace; rpl::event_stream<> _finished; + rpl::event_stream<> _textPossiblyChanged; // Must be above _masked. InputField *_input = nullptr; MaskedInputField *_masked = nullptr; QString _countryIso2; diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index b1aa3a2b6..10cada6de 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -279,7 +279,9 @@ void FormSummary::setupPrices(not_null layout) { add(tr::lng_payments_tips_label(tr::now), tips); } } else if (_invoice.tipsMax > 0) { - const auto text = formatAmount(_invoice.tipsSelected); + const auto text = _invoice.tipsSelected + ? formatAmount(_invoice.tipsSelected) + : tr::lng_payments_tips_add(tr::now); const auto label = addRow( tr::lng_payments_tips_label(tr::now), Ui::Text::Link(text, "internal:edit_tips")); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index cf74d02cc..0f457fe47 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_field.h" #include "ui/widgets/separate_panel.h" #include "ui/widgets/checkbox.h" +#include "ui/wrap/fade_wrap.h" #include "ui/boxes/single_choice_box.h" #include "ui/text/format_values.h" #include "lang/lang_keys.h" @@ -23,18 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" namespace Payments::Ui { -namespace { - -[[nodiscard]] auto PriceAmountValidator(int64 min, int64 max) { - return [=](FieldValidateRequest request) { - return FieldValidateResult{ - .value = request.nowValue, - .position = request.nowPosition, - }; - }; -} - -} // namespace Panel::Panel(not_null delegate) : _delegate(delegate) @@ -193,27 +182,52 @@ void Panel::chooseTips(const Invoice &invoice) { const auto min = invoice.tipsMin; const auto max = invoice.tipsMax; const auto now = invoice.tipsSelected; + const auto currency = invoice.currency; showBox(Box([=](not_null box) { - box->setTitle(tr::lng_payments_tips_title()); - + box->setTitle(tr::lng_payments_tips_box_title()); const auto row = box->lifetime().make_state( box, FieldConfig{ - .type = FieldType::PriceAmount, - .placeholder = tr::lng_payments_tips_enter(), + .type = FieldType::Money, .value = QString::number(now), - .validator = PriceAmountValidator(min, max), + .currency = ([&]() -> QString { + static auto counter = 0; + switch (++counter % 9) { + case 0: return "USD"; + case 1: return "EUR"; + case 2: return "IRR"; + case 3: return "BRL"; + case 4: return "ALL"; + case 5: return "AZN"; + case 6: return "CHF"; + case 7: return "DKK"; + case 8: return "KZT"; + } + return currency; + })(), // #TODO payments currency, }); box->setFocusCallback([=] { row->setFocusFast(); }); box->addRow(row->ownedWidget()); - box->addRow(object_ptr(box, "Min: " + QString::number(min), st::defaultFlatLabel)); - box->addRow(object_ptr(box, "Max: " + QString::number(max), st::defaultFlatLabel)); + const auto errorWrap = box->addRow( + object_ptr>( + box, + object_ptr( + box, + tr::lng_payments_tips_max( + lt_amount, + rpl::single(FillAmountAndCurrency(max, currency))), + st::paymentTipsErrorLabel)), + st::paymentTipsErrorPadding); + errorWrap->hide(anim::type::instant); box->addButton(tr::lng_settings_save(), [=] { const auto value = row->value().toLongLong(); - if (value < min || value > max) { + if (value < min) { row->showError(); + } else if (value > max) { + row->showError(); + errorWrap->show(anim::type::normal); } else { _delegate->panelChangeTips(value); box->closeBox(); diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index 9294cc0bc..d469102ec 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -45,40 +45,6 @@ namespace { return phrase(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb); } -[[nodiscard]] QString FormatWithSeparators( - double amount, - int precision, - char decimal, - char thousands) { - Expects(decimal != 0); - - // Thanks https://stackoverflow.com/a/5058949 - struct FormattingHelper : std::numpunct { - FormattingHelper(char decimal, char thousands) - : decimal(decimal) - , thousands(thousands) { - } - - char do_decimal_point() const override { return decimal; } - char do_thousands_sep() const override { return thousands; } - - char decimal = '.'; - char thousands = ','; - }; - - auto stream = std::ostringstream(); - stream.imbue(std::locale( - stream.getloc(), - new FormattingHelper(decimal, thousands ? thousands : '?'))); - stream.precision(precision); - stream << std::fixed << amount; - auto result = QString::fromStdString(stream.str()); - if (!thousands) { - result.replace('?', QString()); - } - return result; -} - } // namespace QString FormatSizeText(qint64 size) { @@ -162,16 +128,37 @@ QString FormatPlayedText(qint64 played, qint64 duration) { } QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { - struct Rule { - //const char *name = ""; - //const char *native = ""; - const char *international = ""; - char thousands = ','; - char decimal = '.'; - bool left = true; - bool space = false; - }; - static const auto kRules = std::vector>{ + const auto rule = LookupCurrencyRule(currency); + + const auto prefix = (amount < 0) + ? QString::fromUtf8("\xe2\x88\x92") + : QString(); + const auto value = std::abs(amount) / std::pow(10., rule.exponent); + const auto name = (*rule.international) + ? QString::fromUtf8(rule.international) + : currency; + auto result = prefix; + if (rule.left) { + result.append(name); + if (rule.space) result.append(' '); + } + const auto precision = (!rule.stripDotZero || std::floor(value) != value) + ? rule.exponent + : 0; + result.append(FormatWithSeparators( + value, + precision, + rule.decimal, + rule.thousands)); + if (!rule.left) { + if (rule.space) result.append(' '); + result.append(name); + } + return result; +} + +CurrencyRule LookupCurrencyRule(const QString ¤cy) { + static const auto kRules = std::vector>{ { u"AED"_q, { "", ',', '.', true, true } }, { u"AFN"_q, {} }, { u"ALL"_q, { "", '.', ',', false } }, @@ -185,11 +172,11 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"BND"_q, { "", '.', ',', } }, { u"BOB"_q, { "", '.', ',', true, true } }, { u"BRL"_q, { "R$", '.', ',', true, true } }, - { u"BHD"_q, { "", ',', '.', true, true } }, - { u"BYR"_q, { "", ' ', ',', false, true } }, + { u"BHD"_q, { "", ',', '.', true, true, 3 } }, + { u"BYR"_q, { "", ' ', ',', false, true, 0 } }, { u"CAD"_q, { "CA$" } }, { u"CHF"_q, { "", '\'', '.', false, true } }, - { u"CLP"_q, { "", '.', ',', true, true } }, + { u"CLP"_q, { "", '.', ',', true, true, 0 } }, { u"CNY"_q, { "\x43\x4E\xC2\xA5" } }, { u"COP"_q, { "", '.', ',', true, true } }, { u"CRC"_q, { "", '.', ',', } }, @@ -209,12 +196,12 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"IDR"_q, { "", '.', ',', } }, { u"ILS"_q, { "\xE2\x82\xAA", ',', '.', true, true } }, { u"INR"_q, { "\xE2\x82\xB9" } }, - { u"ISK"_q, { "", '.', ',', false, true } }, + { u"ISK"_q, { "", '.', ',', false, true, 0 } }, { u"JMD"_q, {} }, - { u"JPY"_q, { "\xC2\xA5" } }, + { u"JPY"_q, { "\xC2\xA5", ',', '.', true, false, 0 } }, { u"KES"_q, {} }, { u"KGS"_q, { "", ' ', '-', false, true } }, - { u"KRW"_q, { "\xE2\x82\xA9" } }, + { u"KRW"_q, { "\xE2\x82\xA9", ',', '.', true, false, 0 } }, { u"KZT"_q, { "", ' ', '-', } }, { u"LBP"_q, { "", ',', '.', true, true } }, { u"LKR"_q, { "", ',', '.', true, true } }, @@ -236,7 +223,7 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"PHP"_q, {} }, { u"PKR"_q, {} }, { u"PLN"_q, { "", ' ', ',', false, true } }, - { u"PYG"_q, { "", '.', ',', true, true } }, + { u"PYG"_q, { "", '.', ',', true, true, 0 } }, { u"QAR"_q, { "", ',', '.', true, true } }, { u"RON"_q, { "", '.', ',', false, true } }, { u"RSD"_q, { "", '.', ',', false, true } }, @@ -251,27 +238,27 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { { u"TWD"_q, { "NT$" } }, { u"TZS"_q, {} }, { u"UAH"_q, { "", ' ', ',', false } }, - { u"UGX"_q, {} }, + { u"UGX"_q, { "", ',', '.', true, false, 0 } }, { u"USD"_q, { "$" } }, { u"UYU"_q, { "", '.', ',', true, true } }, { u"UZS"_q, { "", ' ', ',', false, true } }, - { u"VND"_q, { "\xE2\x82\xAB", '.', ',', false, true } }, + { u"VND"_q, { "\xE2\x82\xAB", '.', ',', false, true, 0 } }, { u"YER"_q, { "", ',', '.', true, true } }, { u"ZAR"_q, { "", ',', '.', true, true } }, - { u"IRR"_q, { "", ',', '/', false, true } }, - { u"IQD"_q, { "", ',', '.', true, true } }, + { u"IRR"_q, { "", ',', '/', false, true, 2, true } }, + { u"IQD"_q, { "", ',', '.', true, true, 3 } }, { u"VEF"_q, { "", '.', ',', true, true } }, { u"SYP"_q, { "", ',', '.', true, true } }, - //{ u"VUV"_q, { "", ',', '.', false } }, + //{ u"VUV"_q, { "", ',', '.', false, false, 0 } }, //{ u"WST"_q, {} }, - //{ u"XAF"_q, { "FCFA", ',', '.', false } }, + //{ u"XAF"_q, { "FCFA", ',', '.', false, false, 0 } }, //{ u"XCD"_q, {} }, - //{ u"XOF"_q, { "CFA", ' ', ',', false } }, - //{ u"XPF"_q, { "", ',', '.', false } }, + //{ u"XOF"_q, { "CFA", ' ', ',', false, false, 0 } }, + //{ u"XPF"_q, { "", ',', '.', false, false, 0 } }, //{ u"ZMW"_q, {} }, //{ u"ANG"_q, {} }, - //{ u"RWF"_q, { "", ' ', ',', true, true } }, + //{ u"RWF"_q, { "", ' ', ',', true, true, 0 } }, //{ u"PGK"_q, {} }, //{ u"TOP"_q, {} }, //{ u"SBD"_q, {} }, @@ -286,109 +273,85 @@ QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { //{ u"AOA"_q, {} }, //{ u"AWG"_q, {} }, //{ u"BBD"_q, {} }, - //{ u"BIF"_q, { "", ',', '.', false } }, + //{ u"BIF"_q, { "", ',', '.', false, false, 0 } }, //{ u"BMD"_q, {} }, //{ u"BSD"_q, {} }, //{ u"BWP"_q, {} }, //{ u"BZD"_q, {} }, //{ u"CDF"_q, { "", ',', '.', false } }, - //{ u"CVE"_q, {} }, - //{ u"DJF"_q, { "", ',', '.', false } }, + //{ u"CVE"_q, { "", ',', '.', true, false, 0 } }, + //{ u"DJF"_q, { "", ',', '.', false, false, 0 } }, //{ u"ETB"_q, {} }, //{ u"FJD"_q, {} }, //{ u"FKP"_q, {} }, //{ u"GIP"_q, {} }, //{ u"GMD"_q, { "", ',', '.', false } }, - //{ u"GNF"_q, { "", ',', '.', false } }, + //{ u"GNF"_q, { "", ',', '.', false, false, 0 } }, //{ u"GYD"_q, {} }, //{ u"HTG"_q, {} }, //{ u"KHR"_q, { "", ',', '.', false } }, - //{ u"KMF"_q, { "", ',', '.', false } }, + //{ u"KMF"_q, { "", ',', '.', false, false, 0 } }, //{ u"KYD"_q, {} }, //{ u"LAK"_q, { "", ',', '.', false } }, //{ u"LRD"_q, {} }, //{ u"LSL"_q, { "", ',', '.', false } }, - //{ u"MGA"_q, {} }, + //{ u"MGA"_q, { "", ',', '.', true, false, 0 } }, //{ u"MKD"_q, { "", '.', ',', false, true } }, //{ u"MOP"_q, {} }, //{ u"MWK"_q, {} }, //{ u"NAD"_q, {} }, + //{ u"CLF"_q, { "", ',', '.', true, false, 4 } }, + //{ u"JOD"_q, { "", ',', '.', true, false, 3 } }, + //{ u"KWD"_q, { "", ',', '.', true, false, 3 } }, + //{ u"LYD"_q, { "", ',', '.', true, false, 3 } }, + //{ u"OMR"_q, { "", ',', '.', true, false, 3 } }, + //{ u"TND"_q, { "", ',', '.', true, false, 3 } }, + //{ u"UYI"_q, { "", ',', '.', true, false, 0 } }, + //{ u"MRO"_q, { "", ',', '.', true, false, 1 } }, }; static const auto kRulesMap = [] { // flat_multi_map_pair_type lacks some required constructors :( - auto &&pairs = kRules | ranges::views::transform([](auto &&pair) { - return base::flat_multi_map_pair_type( + auto &&list = kRules | ranges::views::transform([](auto &&pair) { + return base::flat_multi_map_pair_type( pair.first, pair.second); }); - return base::flat_map(begin(pairs), end(pairs)); + return base::flat_map(begin(list), end(list)); }(); - static const auto kExponents = base::flat_map{ - { u"CLF"_q, 4 }, - { u"BHD"_q, 3 }, - { u"IQD"_q, 3 }, - { u"JOD"_q, 3 }, - { u"KWD"_q, 3 }, - { u"LYD"_q, 3 }, - { u"OMR"_q, 3 }, - { u"TND"_q, 3 }, - { u"BIF"_q, 0 }, - { u"BYR"_q, 0 }, - { u"CLP"_q, 0 }, - { u"CVE"_q, 0 }, - { u"DJF"_q, 0 }, - { u"GNF"_q, 0 }, - { u"ISK"_q, 0 }, - { u"JPY"_q, 0 }, - { u"KMF"_q, 0 }, - { u"KRW"_q, 0 }, - { u"MGA"_q, 0 }, - { u"PYG"_q, 0 }, - { u"RWF"_q, 0 }, - { u"UGX"_q, 0 }, - { u"UYI"_q, 0 }, - { u"VND"_q, 0 }, - { u"VUV"_q, 0 }, - { u"XAF"_q, 0 }, - { u"XOF"_q, 0 }, - { u"XPF"_q, 0 }, - { u"MRO"_q, 1 }, + const auto i = kRulesMap.find(currency); + return (i != end(kRulesMap)) ? i->second : CurrencyRule{}; +} + +[[nodiscard]] QString FormatWithSeparators( + double amount, + int precision, + char decimal, + char thousands) { + Expects(decimal != 0); + + // Thanks https://stackoverflow.com/a/5058949 + struct FormattingHelper : std::numpunct { + FormattingHelper(char decimal, char thousands) + : decimal(decimal) + , thousands(thousands) { + } + + char do_decimal_point() const override { return decimal; } + char do_thousands_sep() const override { return thousands; } + + char decimal = '.'; + char thousands = ','; }; - const auto prefix = (amount < 0) - ? QString::fromUtf8("\xe2\x88\x92") - : QString(); - - const auto exponentIt = kExponents.find(currency); - const auto exponent = (exponentIt != end(kExponents)) - ? exponentIt->second - : 2; - const auto value = std::abs(amount) / std::pow(10., exponent); - const auto ruleIt = kRulesMap.find(currency); - if (ruleIt == end(kRulesMap)) { - return prefix + QLocale::system().toCurrencyString(value, currency); - } - const auto &rule = ruleIt->second; - const auto name = (*rule.international) - ? QString::fromUtf8(rule.international) - : currency; - auto result = prefix; - if (rule.left) { - result.append(name); - if (rule.space) result.append(' '); - } - const auto precision = (currency != u"IRR"_q - || std::floor(value) != value) - ? exponent - : 0; - result.append(FormatWithSeparators( - value, - precision, - rule.decimal, - rule.thousands)); - if (!rule.left) { - if (rule.space) result.append(' '); - result.append(name); + auto stream = std::ostringstream(); + stream.imbue(std::locale( + stream.getloc(), + new FormattingHelper(decimal, thousands ? thousands : '?'))); + stream.precision(precision); + stream << std::fixed << amount; + auto result = QString::fromStdString(stream.str()); + if (!thousands) { + result.replace('?', QString()); } return result; } diff --git a/Telegram/SourceFiles/ui/text/format_values.h b/Telegram/SourceFiles/ui/text/format_values.h index 779d3e7ce..0bfb69d9c 100644 --- a/Telegram/SourceFiles/ui/text/format_values.h +++ b/Telegram/SourceFiles/ui/text/format_values.h @@ -23,9 +23,25 @@ inline constexpr auto FileStatusSizeFailed = 0x7FFFFFF2; [[nodiscard]] QString FormatGifAndSizeText(qint64 size); [[nodiscard]] QString FormatPlayedText(qint64 played, qint64 duration); +struct CurrencyRule { + const char *international = ""; + char thousands = ','; + char decimal = '.'; + bool left = true; + bool space = false; + int exponent = 2; + bool stripDotZero = false; +}; + [[nodiscard]] QString FillAmountAndCurrency( int64 amount, const QString ¤cy); +[[nodiscard]] CurrencyRule LookupCurrencyRule(const QString ¤cy); +[[nodiscard]] QString FormatWithSeparators( + double amount, + int precision, + char decimal, + char thousands); [[nodiscard]] QString ComposeNameString( const QString &filename, From 7cbe158d00e0cf8433ba0e07ead90adda64c3926 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 15:58:39 +0400 Subject: [PATCH 040/127] Update API scheme. --- Telegram/Resources/tl/api.tl | 14 +++++---- .../SourceFiles/calls/calls_group_call.cpp | 5 +++- .../export/data/export_data_types.cpp | 16 ++++++---- .../export/data/export_data_types.h | 12 +++++++- .../export/output/export_output_html.cpp | 30 ++++++++++++++++--- .../export/output/export_output_json.cpp | 8 +++++ .../SourceFiles/history/history_service.cpp | 11 +++++++ .../SourceFiles/payments/payments_form.cpp | 16 +++++----- .../payments/ui/payments_panel.cpp | 5 +--- .../payments/ui/payments_panel_data.h | 2 +- 10 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 6124140f9..93d30d008 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -68,7 +68,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; +inputMediaInvoice#f4e096c3 flags:# multiple_allowed:flags.1?true can_forward:flags.2?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -186,6 +186,7 @@ messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction; messageActionInviteToGroupCall#76b9f11a call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#aa1afbfd period:int = MessageAction; +messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -648,7 +649,7 @@ inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:f inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaInvoice#d5348d85 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaInvoice#d5348d85 flags:# multiple_allowed:flags.1?true can_forward:flags.3?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; @@ -794,7 +795,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#24b6f6cd flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true multiple_allowed:flags.9?true can_forward:flags.10?true currency:string prices:Vector min_tip_amount:flags.8?long max_tip_amount:flags.8?long default_tip_amount:flags.8?long = Invoice; +invoice#cd886e0 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -1203,7 +1204,7 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#c0c2052e flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true id:long access_hash:long participants_count:int params:flags.0?DataJSON title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int version:int = GroupCall; +groupCall#c95c6654 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true id:long access_hash:long participants_count:int params:flags.0?DataJSON title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; @@ -1615,7 +1616,7 @@ phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall durati phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates; phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; -phone.createGroupCall#bd3dabe0 peer:InputPeer random_id:int = Updates; +phone.createGroupCall#48cdc6d8 flags:# peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; @@ -1629,6 +1630,9 @@ phone.editGroupCallParticipant#d975eb80 flags:# muted:flags.0?true call:InputGro phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates; phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers; phone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite; +phone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates; +phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates; +phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index 1faa1336a..8c0d7ff2a 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -328,8 +328,11 @@ bool GroupCall::showChooseJoinAs() const { void GroupCall::start() { _createRequestId = _api.request(MTPphone_CreateGroupCall( + MTP_flags(0), _peer->input, - MTP_int(openssl::RandomValue()) + MTP_int(openssl::RandomValue()), + MTPstring(), // title + MTPint() // schedule_date )).done([=](const MTPUpdates &result) { _acceptFields = true; _peer->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 8bc8950d9..c1359347f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1129,22 +1129,28 @@ ServiceAction ParseServiceAction( } result.content = content; }, [&](const MTPDmessageActionSetMessagesTTL &data) { - // #TODO ttl + result.content = ActionSetMessagesTTL{ + .period = data.vperiod().v, + }; + }, [&](const MTPDmessageActionGroupCallScheduled &data) { + result.content = ActionGroupCallScheduled{ + .date = data.vschedule_date().v, + }; }, [](const MTPDmessageActionEmpty &data) {}); return result; } File &Message::file() { - const auto service = &action.content; - if (const auto photo = std::get_if(service)) { + const auto content = &action.content; + if (const auto photo = std::get_if(content)) { return photo->photo.image.file; } return media.file(); } const File &Message::file() const { - const auto service = &action.content; - if (const auto photo = std::get_if(service)) { + const auto content = &action.content; + if (const auto photo = std::get_if(content)) { return photo->photo.image.file; } return media.file(); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 694c941db..87c49e2da 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -466,6 +466,14 @@ struct ActionInviteToGroupCall { std::vector userIds; }; +struct ActionSetMessagesTTL { + TimeId period = 0; +}; + +struct ActionGroupCallScheduled { + TimeId date = 0; +}; + struct ServiceAction { std::variant< v::null_t, @@ -492,7 +500,9 @@ struct ServiceAction { ActionPhoneNumberRequest, ActionGeoProximityReached, ActionGroupCall, - ActionInviteToGroupCall> content; + ActionInviteToGroupCall, + ActionSetMessagesTTL, + ActionGroupCallScheduled> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 4ed0cdcd8..8c4ac8b99 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1082,15 +1082,37 @@ auto HtmlWriter::Wrap::pushMessage( }, [&](const ActionPhoneNumberRequest &data) { return serviceFrom + " requested your phone number"; }, [&](const ActionGroupCall &data) { - return "Group call" - + (data.duration - ? (" (" + QString::number(data.duration) + " seconds)") - : QString()).toUtf8(); + const auto durationText = (data.duration + ? (" (" + QString::number(data.duration) + " seconds)") + : QString()).toUtf8(); + return isChannel + ? ("Voice chat" + durationText) + : (serviceFrom + " started voice chat" + durationText); }, [&](const ActionInviteToGroupCall &data) { return serviceFrom + " invited " + peers.wrapUserNames(data.userIds) + " to the voice chat"; + }, [&](const ActionSetMessagesTTL &data) { + const auto periodText = (data.period == 7 * 86400) + ? "7 days" + : (data.period == 86400) + ? "24 hours" + : QByteArray(); + return isChannel + ? (data.period + ? "New messages will auto-delete in " + periodText + : "New messages will not auto-delete") + : (data.period + ? (serviceFrom + + " has set messages to auto-delete in " + periodText) + : (serviceFrom + + " has set messages not to auto-delete")); + }, [&](const ActionGroupCallScheduled &data) { + const auto dateText = FormatDateTime(data.date); + return isChannel + ? "Voice chat is scheduled " + dateText + : (serviceFrom + " scheduled voice chat " + dateText); }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index c0e9390ed..903f5702a 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -497,6 +497,14 @@ QByteArray SerializeMessage( pushActor(); pushAction("invite_to_group_call"); pushUserNames(data.userIds); + }, [&](const ActionSetMessagesTTL &data) { + pushActor(); + pushAction("set_messages_ttl"); + push("period", data.period); + }, [&](const ActionGroupCallScheduled &data) { + pushActor(); + pushAction("group_call_scheduled"); + push("schedule_date", data.date); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 03912ae23..51cfb26a9 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -405,6 +405,15 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareGroupCallScheduled = [this](const MTPDmessageActionGroupCallScheduled &action) { + const auto callId = CallIdFromInput(action.vcall()); + const auto peer = history()->peer; + const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false) + ? callId + : 0; + return prepareStartedCallText(linkCallId); + }; + const auto messageText = action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); @@ -460,6 +469,8 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { return prepareInviteToGroupCall(data); }, [&](const MTPDmessageActionSetMessagesTTL &data) { return prepareSetMessagesTTL(data); + }, [&](const MTPDmessageActionGroupCallScheduled &data) { + return prepareGroupCallScheduled(data); }, [](const MTPDmessageActionEmpty &) { return PreparedText{ tr::lng_message_empty(tr::now) }; }); diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index d6cf8976f..660f731bc 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -293,14 +293,19 @@ void Form::processReceipt(const MTPDpayments_paymentReceipt &data) { } void Form::processInvoice(const MTPDinvoice &data) { + const auto suggested = data.vsuggested_tip_amounts().value_or_empty(); _invoice = Ui::Invoice{ .cover = std::move(_invoice.cover), .prices = ParsePrices(data.vprices()), - .tipsMin = ParsePriceAmount(data.vmin_tip_amount().value_or_empty()), + .suggestedTips = ranges::views::all( + suggested + ) | ranges::views::transform( + &MTPlong::v + ) | ranges::views::transform( + ParsePriceAmount + ) | ranges::to_vector, .tipsMax = ParsePriceAmount(data.vmax_tip_amount().value_or_empty()), - .tipsSelected = ParsePriceAmount( - data.vdefault_tip_amount().value_or_empty()), .currency = qs(data.vcurrency()), .isNameRequested = data.is_name_requested(), @@ -746,10 +751,7 @@ void Form::setShippingOption(const QString &id) { } void Form::setTips(int64 value) { - _invoice.tipsSelected = std::clamp( - value, - _invoice.tipsMin, - _invoice.tipsMax); + _invoice.tipsSelected = std::min(value, _invoice.tipsMax); } void Form::processShippingOptions(const QVector &data) { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 0f457fe47..c49819c3e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -179,7 +179,6 @@ void Panel::chooseShippingOption(const ShippingOptions &options) { } void Panel::chooseTips(const Invoice &invoice) { - const auto min = invoice.tipsMin; const auto max = invoice.tipsMax; const auto now = invoice.tipsSelected; const auto currency = invoice.currency; @@ -223,9 +222,7 @@ void Panel::chooseTips(const Invoice &invoice) { errorWrap->hide(anim::type::instant); box->addButton(tr::lng_settings_save(), [=] { const auto value = row->value().toLongLong(); - if (value < min) { - row->showError(); - } else if (value > max) { + if (value > max) { row->showError(); errorWrap->show(anim::type::normal); } else { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index f833703c0..bfd8b78fe 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -39,7 +39,7 @@ struct Invoice { Cover cover; std::vector prices; - int64 tipsMin = 0; + std::vector suggestedTips; int64 tipsMax = 0; int64 tipsSelected = 0; QString currency; From cd4a9d7c1640463400bb540ddec5d5c84e068c00 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 18:05:46 +0400 Subject: [PATCH 041/127] Show 'phone/email passed to provider' in payments. --- Telegram/Resources/langs/lang.strings | 3 +++ .../SourceFiles/payments/payments_form.cpp | 9 +++++++-- .../SourceFiles/payments/ui/payments.style | 3 +++ .../payments/ui/payments_edit_information.cpp | 18 ++++++++++++++++++ .../payments/ui/payments_panel_data.h | 1 + 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9a59fa5c2..0594f9d1b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1879,6 +1879,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_info_name" = "Name"; "lng_payments_info_email" = "Email"; "lng_payments_info_phone" = "Phone"; +"lng_payments_to_provider_phone_email" = "Phone and Email will be passed to {bot_name} as billing info."; +"lng_payments_to_provider_email" = "Email will be passed to {bot_name} as billing info."; +"lng_payments_to_provider_phone" = "Phone will be passed to {bot_name} as billing info."; "lng_payments_shipping_address_title" = "Shipping Information"; "lng_payments_card_title" = "New Card"; "lng_payments_card_number" = "Card Number"; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 660f731bc..614a81232 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -336,11 +336,16 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { .canSaveCredentials = data.is_can_save_credentials(), .passwordMissing = data.is_password_missing(), }; - if (_details.botId) { - if (const auto bot = _session->data().userLoaded(_details.botId)) { + if (const auto botId = _details.botId) { + if (const auto bot = _session->data().userLoaded(botId)) { _invoice.cover.seller = bot->name; } } + if (const auto providerId = _details.providerId) { + if (const auto bot = _session->data().userLoaded(providerId)) { + _invoice.provider = bot->name; + } + } } void Form::processDetails(const MTPDpayments_paymentReceipt &data) { diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 5a7ba8214..737c7b836 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -89,3 +89,6 @@ paymentTipsErrorLabel: FlatLabel(defaultFlatLabel) { textFg: boxTextFgError; } paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px); + +paymentsToProviderLabel: paymentsShippingPrice; +paymentsToProviderPadding: margins(28px, 6px, 28px, 6px); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 04940544e..486842957 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -176,6 +176,24 @@ not_null EditInformation::setupContent() { .defaultPhone = _information.defaultPhone, }); } + const auto emailToProvider = _invoice.isEmailRequested + && _invoice.emailSentToProvider; + const auto phoneToProvider = _invoice.isPhoneRequested + && _invoice.phoneSentToProvider; + if (emailToProvider || phoneToProvider) { + inner->add( + object_ptr( + inner, + ((emailToProvider && phoneToProvider) + ? tr::lng_payments_to_provider_phone_email + : emailToProvider + ? tr::lng_payments_to_provider_email + : tr::lng_payments_to_provider_phone)( + lt_bot_name, + rpl::single(_invoice.provider)), + st::paymentsToProviderLabel), + st::paymentsToProviderPadding); + } _save = inner->add( object_ptr( inner, diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index bfd8b78fe..6df01a54d 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -52,6 +52,7 @@ struct Invoice { bool isFlexible = false; bool isTest = false; + QString provider; bool phoneSentToProvider = false; bool emailSentToProvider = false; From 491ec2db7f554f180898fab1bd6d96d9bcdd2dfe Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 18:39:44 +0400 Subject: [PATCH 042/127] Always show footer in webview in payments. --- Telegram/Resources/langs/lang.strings | 9 ++++-- .../payments/payments_checkout_process.cpp | 5 +++- .../SourceFiles/payments/payments_form.cpp | 1 + .../SourceFiles/payments/ui/payments.style | 3 ++ .../payments/ui/payments_edit_information.cpp | 2 +- .../payments/ui/payments_panel.cpp | 30 +++++++++++++++++-- .../SourceFiles/payments/ui/payments_panel.h | 5 +++- .../payments/ui/payments_panel_data.h | 1 + 8 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0594f9d1b..607d83829 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1879,9 +1879,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_info_name" = "Name"; "lng_payments_info_email" = "Email"; "lng_payments_info_phone" = "Phone"; -"lng_payments_to_provider_phone_email" = "Phone and Email will be passed to {bot_name} as billing info."; -"lng_payments_to_provider_email" = "Email will be passed to {bot_name} as billing info."; -"lng_payments_to_provider_phone" = "Phone will be passed to {bot_name} as billing info."; +"lng_payments_to_provider_phone_email" = "Phone and Email will be passed to {provider} as billing info."; +"lng_payments_to_provider_email" = "Email will be passed to {provider} as billing info."; +"lng_payments_to_provider_phone" = "Phone will be passed to {provider} as billing info."; +"lng_payments_processed_by" = "Processed by {provider}"; +"lng_payments_warning_title" = "Warning"; +"lng_payments_warning_body" = "Neither Telegram, nor {bot1} will have access to your credit card information. Credit card details will be handled only by the payment system, {provider}.\n\nPayments will go directly to the developer of {bot2}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {bot3} or your bank."; "lng_payments_shipping_address_title" = "Shipping Information"; "lng_payments_card_title" = "New Card"; "lng_payments_card_number" = "Card Number"; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 290b7762c..878f9066b 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -167,7 +167,10 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { _submitState = SubmitState::Validated; requestPassword(); }, [&](const VerificationNeeded &data) { - if (!_panel->showWebview(data.url, false)) { + auto bottomText = tr::lng_payments_processed_by( + lt_provider, + rpl::single(_form->invoice().provider)); + if (!_panel->showWebview(data.url, false, std::move(bottomText))) { File::OpenUrl(data.url); close(); } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 614a81232..b2133bbe0 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -405,6 +405,7 @@ void Form::refreshPaymentMethodDetails() { const auto &saved = _paymentMethod.savedCredentials; const auto &entered = _paymentMethod.newCredentials; _paymentMethod.ui.title = entered ? entered.title : saved.title; + _paymentMethod.ui.provider = _invoice.provider; _paymentMethod.ui.ready = entered || saved; _paymentMethod.ui.native.defaultCountry = defaultCountry(); _paymentMethod.ui.canSaveInformation diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 737c7b836..7ec1a7149 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -28,6 +28,9 @@ paymentsTitle: FlatLabel(paymentsDescription) { paymentsSeller: FlatLabel(paymentsDescription) { textFg: windowSubTextFg; } +paymentsWebviewBottom: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} paymentsPriceLabel: paymentsDescription; paymentsPriceAmount: defaultFlatLabel; paymentsFullPriceLabel: paymentsTitle; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index 486842957..d62abd1ad 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -189,7 +189,7 @@ not_null EditInformation::setupContent() { : emailToProvider ? tr::lng_payments_to_provider_email : tr::lng_payments_to_provider_phone)( - lt_bot_name, + lt_provider, rpl::single(_invoice.provider)), st::paymentsToProviderLabel), st::paymentsToProviderPadding); diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index c49819c3e..885e74a00 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -235,10 +235,15 @@ void Panel::chooseTips(const Invoice &invoice) { } void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { + auto bottomText = method.canSaveInformation + ? rpl::producer() + : tr::lng_payments_processed_by( + lt_provider, + rpl::single(method.provider)); _widget->setTitle(tr::lng_payments_card_title()); if (method.native.supported) { showEditCard(method.native, CardField::Number); - } else if (!showWebview(method.url, true)) { + } else if (!showWebview(method.url, true, std::move(bottomText))) { // #TODO payments errors not supported } else if (method.canSaveInformation) { const auto &padding = st::paymentsPanelPadding; @@ -255,12 +260,33 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { } } -bool Panel::showWebview(const QString &url, bool allowBack) { +bool Panel::showWebview( + const QString &url, + bool allowBack, + rpl::producer bottomText) { if (!_webview && !createWebview()) { return false; } _webview->navigate(url); _widget->setBackAllowed(allowBack); + if (bottomText) { + const auto &padding = st::paymentsPanelPadding; + const auto label = CreateChild( + _webviewBottom.get(), + std::move(bottomText), + st::paymentsWebviewBottom); + const auto height = padding.top() + + label->heightNoMargins() + + padding.bottom(); + rpl::combine( + _webviewBottom->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outerWidth, int width) { + label->move((outerWidth - width) / 2, padding.top()); + }, label->lifetime()); + label->show(); + _webviewBottom->resize(_webviewBottom->width(), height); + } return true; } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 488419a54..01cb5a490 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -69,7 +69,10 @@ public: void choosePaymentMethod(const PaymentMethodDetails &method); void askSetPassword(); - bool showWebview(const QString &url, bool allowBack); + bool showWebview( + const QString &url, + bool allowBack, + rpl::producer bottomText); [[nodiscard]] rpl::producer<> backRequests() const; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 6df01a54d..77ae15bc1 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -162,6 +162,7 @@ struct PaymentMethodDetails { QString title; NativeMethodDetails native; QString url; + QString provider; bool ready = false; bool canSaveInformation = false; }; From bdffdea358d6964f3485259311cc339d02a9ecfb Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 19:06:48 +0400 Subject: [PATCH 043/127] Always jump to next field in payments. --- .../payments/ui/payments_edit_card.cpp | 31 +++++++----- .../payments/ui/payments_edit_information.cpp | 14 ++++++ .../payments/ui/payments_field.cpp | 47 +++++++++++++++++-- .../SourceFiles/payments/ui/payments_field.h | 6 +++ 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index d084cc1bd..dbd6e4c2d 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -276,8 +276,18 @@ not_null EditCard::setupContent() { const auto showBox = [=](object_ptr box) { _delegate->panelShowBox(std::move(box)); }; + auto last = (Field*)nullptr; + const auto make = [&](QWidget *parent, FieldConfig &&config) { + auto result = std::make_unique(parent, std::move(config)); + if (last) { + last->setNextField(result.get()); + result->setPreviousField(last); + } + last = result.get(); + return result; + }; const auto add = [&](FieldConfig &&config) { - auto result = std::make_unique(inner, std::move(config)); + auto result = make(inner, std::move(config)); inner->add(result->ownedWidget(), st::paymentsFieldPadding); return result; }; @@ -291,12 +301,12 @@ not_null EditCard::setupContent() { inner, _number->widget()->height()), st::paymentsFieldPadding); - _expire = std::make_unique(container, FieldConfig{ + _expire = make(container, { .type = FieldType::CardExpireDate, .placeholder = rpl::single(u"MM / YY"_q), .validator = ExpireDateValidator(), }); - _cvc = std::make_unique(container, FieldConfig{ + _cvc = make(container, { .type = FieldType::CardCVC, .placeholder = rpl::single(u"CVC"_q), .validator = CvcValidator([=] { return _number->value(); }), @@ -319,15 +329,6 @@ not_null EditCard::setupContent() { }); } - _number->setNextField(_expire.get()); - _expire->setPreviousField(_number.get()); - _expire->setNextField(_cvc.get()); - _cvc->setPreviousField(_expire.get()); - if (_name) { - _cvc->setNextField(_name.get()); - _name->setPreviousField(_cvc.get()); - } - if (_native.needCountry || _native.needZip) { inner->add( object_ptr( @@ -366,6 +367,12 @@ not_null EditCard::setupContent() { false), st::paymentsSaveCheckboxPadding); } + + last->submitted( + ) | rpl::start_with_next([=] { + _delegate->panelValidateCard(collect(), _save && _save->checked()); + }, lifetime()); + return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp index d62abd1ad..ab96a8a9e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -113,9 +113,15 @@ not_null EditInformation::setupContent() { const auto showBox = [=](object_ptr box) { _delegate->panelShowBox(std::move(box)); }; + auto last = (Field*)nullptr; const auto add = [&](FieldConfig &&config) { auto result = std::make_unique(inner, std::move(config)); inner->add(result->ownedWidget(), st::paymentsFieldPadding); + if (last) { + last->setNextField(result.get()); + result->setPreviousField(last); + } + last = result.get(); return result; }; if (_invoice.isShippingAddressRequested) { @@ -200,6 +206,14 @@ not_null EditInformation::setupContent() { tr::lng_payments_save_information(tr::now), true), st::paymentsSaveCheckboxPadding); + + if (last) { + last->submitted( + ) | rpl::start_with_next([=] { + _delegate->panelValidateInformation(collect()); + }, lifetime()); + } + return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 287d6046b..f4d99fccd 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -430,6 +430,7 @@ Field::Field(QWidget *parent, FieldConfig &&config) setupValidator(MoneyValidator(LookupCurrencyRule(config.currency))); } setupFrontBackspace(); + setupSubmit(); } RpWidget *Field::widget() const { @@ -455,6 +456,10 @@ rpl::producer<> Field::finished() const { return _finished.events(); } +rpl::producer<> Field::submitted() const { + return _submitted.events(); +} + void Field::setupMaskedGeometry() { Expects(_masked != nullptr); @@ -492,6 +497,13 @@ void Field::setupCountry() { _masked->setText(Data::CountryNameByISO2(iso2)); _masked->hideError(); raw->closeBox(); + if (!iso2.isEmpty()) { + if (_nextField) { + _nextField->activate(); + } else { + _submitted.fire({}); + } + } }, _masked->lifetime()); raw->boxClosing() | rpl::start_with_next([=] { setFocus(); @@ -563,6 +575,8 @@ void Field::setupValidator(Fn validator) { .nowValue = now.value, .nowPosition = now.position, }); + _valid = result.finished || !result.invalid; + const auto changed = (result.value != now.value); if (changed) { setText(result.value); @@ -609,7 +623,26 @@ void Field::setupFrontBackspace() { } } +void Field::setupSubmit() { + const auto submitted = [=] { + if (!_valid) { + showError(); + } else if (_nextField) { + _nextField->activate(); + } else { + _submitted.fire({}); + } + }; + if (_masked) { + QObject::connect(_masked, &MaskedInputField::submitted, submitted); + } else { + QObject::connect(_input, &InputField::submitted, submitted); + } +} + void Field::setNextField(not_null field) { + _nextField = field; + finished() | rpl::start_with_next([=] { field->setFocus(); }, _masked ? _masked->lifetime() : _input->lifetime()); @@ -622,16 +655,22 @@ void Field::setPreviousField(not_null field) { }, _masked ? _masked->lifetime() : _input->lifetime()); } -void Field::setFocus() { - if (_config.type == FieldType::Country) { - _wrap->setFocus(); - } else if (_input) { +void Field::activate() { + if (_input) { _input->setFocus(); } else { _masked->setFocus(); } } +void Field::setFocus() { + if (_config.type == FieldType::Country) { + _wrap->setFocus(); + } else { + activate(); + } +} + void Field::setFocusFast() { if (_config.type == FieldType::Country) { setFocus(); diff --git a/Telegram/SourceFiles/payments/ui/payments_field.h b/Telegram/SourceFiles/payments/ui/payments_field.h index 5c9c37109..26711ddd9 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.h +++ b/Telegram/SourceFiles/payments/ui/payments_field.h @@ -98,7 +98,9 @@ public: [[nodiscard]] QString value() const; [[nodiscard]] rpl::producer<> frontBackspace() const; [[nodiscard]] rpl::producer<> finished() const; + [[nodiscard]] rpl::producer<> submitted() const; + void activate(); void setFocus(); void setFocusFast(); void showError(); @@ -120,17 +122,21 @@ private: void setupCountry(); void setupValidator(Fn validator); void setupFrontBackspace(); + void setupSubmit(); const FieldConfig _config; const base::unique_qptr _wrap; rpl::event_stream<> _frontBackspace; rpl::event_stream<> _finished; + rpl::event_stream<> _submitted; rpl::event_stream<> _textPossiblyChanged; // Must be above _masked. InputField *_input = nullptr; MaskedInputField *_masked = nullptr; + Field *_nextField = nullptr; QString _countryIso2; State _was; bool _validating = false; + bool _valid = true; }; From b1c122a2600f3c89047e9e53323e889a2baaa1e1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Apr 2021 19:22:01 +0400 Subject: [PATCH 044/127] Add ' (Test)' to checkout panel titles. --- Telegram/SourceFiles/payments/payments_form.cpp | 1 + .../SourceFiles/payments/ui/payments_panel.cpp | 16 +++++++++++++--- .../SourceFiles/payments/ui/payments_panel.h | 2 ++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index b2133bbe0..036b91c96 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -126,6 +126,7 @@ void Form::fillInvoiceFromMessage() { return item->media(); }(); if (const auto invoice = media ? media->invoice() : nullptr) { + _invoice.isTest = invoice->isTest; _invoice.cover = Ui::Cover{ .title = invoice->title, .description = invoice->description, diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 885e74a00..110809014 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -56,7 +56,8 @@ void Panel::showForm( const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options) { - _widget->setTitle(invoice.receipt + _testMode = invoice.isTest; + setTitle(invoice.receipt ? tr::lng_payments_receipt_title() : tr::lng_payments_checkout_title()); auto form = base::make_unique_q( @@ -81,7 +82,7 @@ void Panel::showEditInformation( const Invoice &invoice, const RequestedInformation ¤t, InformationField field) { - _widget->setTitle(tr::lng_payments_shipping_address_title()); + setTitle(tr::lng_payments_shipping_address_title()); auto edit = base::make_unique_q( _widget.get(), invoice, @@ -240,7 +241,7 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) { : tr::lng_payments_processed_by( lt_provider, rpl::single(method.provider)); - _widget->setTitle(tr::lng_payments_card_title()); + setTitle(tr::lng_payments_card_title()); if (method.native.supported) { showEditCard(method.native, CardField::Number); } else if (!showWebview(method.url, true, std::move(bottomText))) { @@ -419,6 +420,15 @@ void Panel::showCardError( } } +void Panel::setTitle(rpl::producer title) { + using namespace rpl::mappers; + if (_testMode) { + _widget->setTitle(std::move(title) | rpl::map(_1 + " (Test)")); + } else { + _widget->setTitle(std::move(title)); + } +} + rpl::producer<> Panel::backRequests() const { return _widget->backRequests(); } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 01cb5a490..afe5d72e5 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -83,6 +83,7 @@ public: private: bool createWebview(); + void setTitle(rpl::producer title); const not_null _delegate; std::unique_ptr _widget; @@ -92,6 +93,7 @@ private: QPointer _weakFormSummary; QPointer _weakEditInformation; QPointer _weakEditCard; + bool _testMode = false; }; From d55d7f37d7e3a526cd7d9213c5b43778e54881d8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Apr 2021 15:15:29 +0400 Subject: [PATCH 045/127] Close payments panel by escape. --- Telegram/SourceFiles/ui/widgets/separate_panel.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp index 798c825d5..69e67fdce 100644 --- a/Telegram/SourceFiles/ui/widgets/separate_panel.cpp +++ b/Telegram/SourceFiles/ui/widgets/separate_panel.cpp @@ -129,8 +129,14 @@ void SeparatePanel::showAndActivate() { } void SeparatePanel::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Escape && _back->toggled()) { - _synteticBackRequests.fire({}); + if (e->key() == Qt::Key_Escape) { + crl::on_main(this, [=] { + if (_back->toggled()) { + _synteticBackRequests.fire({}); + } else { + _userCloseRequests.fire({}); + } + }); } return RpWidget::keyPressEvent(e); } From b6c86fd298db744c0465ce32885b934a46eae7b4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Apr 2021 15:46:48 +0400 Subject: [PATCH 046/127] Add nice tips buttons. --- .../SourceFiles/payments/ui/payments.style | 17 ++ .../payments/ui/payments_form_summary.cpp | 185 ++++++++++++++++-- .../payments/ui/payments_form_summary.h | 18 +- .../payments/ui/payments_panel.cpp | 13 +- .../SourceFiles/payments/ui/payments_panel.h | 1 + .../SourceFiles/ui/text/format_values.cpp | 8 +- Telegram/SourceFiles/ui/text/format_values.h | 3 +- 7 files changed, 225 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 7ec1a7149..fa068a568 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -49,6 +49,23 @@ paymentsPricesTopSkip: 12px; paymentsPricesBottomSkip: 13px; paymentsPricePadding: margins(28px, 6px, 28px, 5px); +paymentsTipSkip: 8px; +paymentsTipButton: RoundButton(defaultLightButton) { + textFg: paymentsTipActive; + textFgOver: paymentsTipActive; + textBgOver: transparent; + + width: -16px; + height: 28px; + textTop: 5px; +} +paymentsTipChosen: RoundButton(paymentsTipButton) { + textFg: windowFgActive; + textFgOver: windowFgActive; + textBgOver: transparent; +} +paymentsTipButtonsPadding: margins(26px, 6px, 26px, 6px); + paymentsSectionsTopSkip: 11px; paymentsSectionButton: SettingsButton(infoProfileButton) { padding: margins(68px, 11px, 14px, 9px); diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 10cada6de..9f60bae26 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -27,6 +27,36 @@ QString formatPhone(QString phone); // #TODO } // namespace App namespace Payments::Ui { +namespace { + +constexpr auto kLightOpacity = 0.1; +constexpr auto kLightRippleOpacity = 0.11; +constexpr auto kChosenOpacity = 0.8; +constexpr auto kChosenRippleOpacity = 0.5; + +[[nodiscard]] Fn TransparentColor( + const style::color &c, + float64 opacity) { + return [&c, opacity] { + return QColor( + c->c.red(), + c->c.green(), + c->c.blue(), + c->c.alpha() * opacity); + }; +} + +[[nodiscard]] style::RoundButton TipButtonStyle( + const style::RoundButton &original, + const style::color &light, + const style::color &ripple) { + auto result = original; + result.textBg = light; + result.ripple.color = ripple; + return result; +} + +} // namespace using namespace ::Ui; @@ -38,7 +68,8 @@ FormSummary::FormSummary( const RequestedInformation ¤t, const PaymentMethodDetails &method, const ShippingOptions &options, - not_null delegate) + not_null delegate, + int scrollTop) : _delegate(delegate) , _invoice(invoice) , _method(method) @@ -56,21 +87,45 @@ FormSummary::FormSummary( rpl::single(formatAmount(computeTotalAmount()))), st::paymentsPanelSubmit)) , _cancel( - this, - (_invoice.receipt.paid - ? tr::lng_about_done() - : tr::lng_cancel()), - st::paymentsPanelButton) { + this, + (_invoice.receipt.paid + ? tr::lng_about_done() + : tr::lng_cancel()), + st::paymentsPanelButton) +, _tipLightBg(TransparentColor(st::paymentsTipActive, kLightOpacity)) +, _tipLightRipple( + TransparentColor(st::paymentsTipActive, kLightRippleOpacity)) +, _tipChosenBg(TransparentColor(st::paymentsTipActive, kChosenOpacity)) +, _tipChosenRipple( + TransparentColor(st::paymentsTipActive, kChosenRippleOpacity)) +, _tipButton(TipButtonStyle( + st::paymentsTipButton, + _tipLightBg.color(), + _tipLightRipple.color())) +, _tipChosen(TipButtonStyle( + st::paymentsTipChosen, + _tipChosenBg.color(), + _tipChosenRipple.color())) +, _initialScrollTop(scrollTop) { setupControls(); } +rpl::producer FormSummary::scrollTopValue() const { + return _scroll->scrollTopValue(); +} + void FormSummary::updateThumbnail(const QImage &thumbnail) { _invoice.cover.thumbnail = thumbnail; _thumbnails.fire_copy(thumbnail); } -QString FormSummary::formatAmount(int64 amount) const { - return FillAmountAndCurrency(amount, _invoice.currency); +QString FormSummary::formatAmount( + int64 amount, + bool forceStripDotZero) const { + return FillAmountAndCurrency( + amount, + _invoice.currency, + forceStripDotZero); } int64 FormSummary::computeTotalAmount() const { @@ -279,9 +334,7 @@ void FormSummary::setupPrices(not_null layout) { add(tr::lng_payments_tips_label(tr::now), tips); } } else if (_invoice.tipsMax > 0) { - const auto text = _invoice.tipsSelected - ? formatAmount(_invoice.tipsSelected) - : tr::lng_payments_tips_add(tr::now); + const auto text = formatAmount(_invoice.tipsSelected); const auto label = addRow( tr::lng_payments_tips_label(tr::now), Ui::Text::Link(text, "internal:edit_tips")); @@ -289,12 +342,116 @@ void FormSummary::setupPrices(not_null layout) { _delegate->panelChooseTips(); return false; }); + setupSuggestedTips(layout); } add(tr::lng_payments_total_label(tr::now), total, true); Settings::AddSkip(layout, st::paymentsPricesBottomSkip); } +void FormSummary::setupSuggestedTips(not_null layout) { + if (_invoice.suggestedTips.empty()) { + return; + } + struct Button { + RoundButton *widget = nullptr; + int minWidth = 0; + }; + struct State { + std::vector