summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Preston <johnprestonmail@gmail.com>2021-03-23 16:34:34 +0400
committerJohn Preston <johnprestonmail@gmail.com>2021-03-23 16:34:34 +0400
commit28137dfb609fb5da649d46ee125ad789a0580fc8 (patch)
tree9c640689e479adbf6f74376abf58003bb74e294e
parente7784620d36b146942cda025228f1aa03cff1dd9 (diff)
Start proper payments processing.
-rw-r--r--Telegram/CMakeLists.txt6
-rw-r--r--Telegram/Resources/langs/lang.strings19
-rw-r--r--Telegram/SourceFiles/core/ui_integration.cpp4
-rw-r--r--Telegram/SourceFiles/core/ui_integration.h2
-rw-r--r--Telegram/SourceFiles/facades.cpp153
-rw-r--r--Telegram/SourceFiles/payments/payments_checkout_process.cpp240
-rw-r--r--Telegram/SourceFiles/payments/payments_checkout_process.h69
-rw-r--r--Telegram/SourceFiles/payments/payments_form.cpp154
-rw-r--r--Telegram/SourceFiles/payments/payments_form.h106
-rw-r--r--Telegram/SourceFiles/payments/ui/payments.style10
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_form_summary.cpp87
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_form_summary.h48
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_panel.cpp47
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_panel.h36
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_panel_data.h86
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_panel_delegate.h24
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_webview.cpp94
-rw-r--r--Telegram/SourceFiles/payments/ui/payments_webview.h37
-rw-r--r--Telegram/SourceFiles/ui/widgets/separate_panel.cpp39
-rw-r--r--Telegram/SourceFiles/ui/widgets/separate_panel.h2
-rw-r--r--Telegram/cmake/td_ui.cmake16
m---------Telegram/lib_ui0
m---------Telegram/lib_webview0
23 files changed, 1105 insertions, 174 deletions
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index a5ac9cdf6b..cb9eec67d5 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -815,6 +815,10 @@ PRIVATE
passport/passport_panel_form.h
passport/passport_panel_password.cpp
passport/passport_panel_password.h
+ payments/payments_checkout_process.cpp
+ payments/payments_checkout_process.h
+ payments/payments_form.cpp
+ payments/payments_form.h
platform/linux/linux_desktop_environment.cpp
platform/linux/linux_desktop_environment.h
platform/linux/linux_gdk_helper.cpp
@@ -1014,8 +1018,6 @@ PRIVATE
ui/widgets/level_meter.h
ui/widgets/multi_select.cpp
ui/widgets/multi_select.h
- ui/widgets/separate_panel.cpp
- ui/widgets/separate_panel.h
ui/countryinput.cpp
ui/countryinput.h
ui/empty_userpic.cpp
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 99b4ff0847..0a2a9c6ae2 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1860,6 +1860,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_invoice_label_test" = "Test invoice";
"lng_payments_receipt_button" = "Receipt";
+"lng_payments_checkout_title" = "Checkout";
+"lng_payments_total_label" = "Total";
+"lng_payments_pay_amount" = "Pay {amount}";
+//"lng_payments_payment_method" = "Payment Method"; // #TODO payments native
+"lng_payments_shipping_address" = "Shipping Information";
+"lng_payments_shipping_method" = "Shipping Method";
+"lng_payments_info_name" = "Name";
+"lng_payments_info_email" = "Email";
+"lng_payments_info_phone" = "Phone";
+"lng_payments_shipping_address_title" = "Shipping Address";
+"lng_payments_save_shipping_about" = "You can save your shipping information for future use.";
+//"lng_payments_payment_card" = "Payment Card"; // #TODO payments native
+//"lng_payments_cardholder_title" = "Cardholder";
+//"lng_payments_cardholder_about" = "Cardholder Name";
+//"lng_payments_billing_address" = "Billing Address";
+//"lng_payments_zip_code" = "Zip Code";
+//"lng_payments_save_payment_about" = "You can save your payment information for future use.";
+"lng_payments_save_information" = "Save Information";
+
"lng_call_status_incoming" = "is calling you...";
"lng_call_status_connecting" = "connecting...";
"lng_call_status_exchanging" = "exchanging encryption keys...";
diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp
index fab5b10705..d2cf492b1f 100644
--- a/Telegram/SourceFiles/core/ui_integration.cpp
+++ b/Telegram/SourceFiles/core/ui_integration.cpp
@@ -126,6 +126,10 @@ QString UiIntegration::timeFormat() {
return cTimeFormat();
}
+QWidget *UiIntegration::modalWindowParent() {
+ return Core::App().getModalParent();
+}
+
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
const EntityLinkData &data,
const std::any &context) {
diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h
index 2aac9a0aa7..5bdc337a46 100644
--- a/Telegram/SourceFiles/core/ui_integration.h
+++ b/Telegram/SourceFiles/core/ui_integration.h
@@ -46,6 +46,8 @@ public:
void startFontsEnd() override;
QString timeFormat() override;
+ QWidget *modalWindowParent() override;
+
std::shared_ptr<ClickHandler> createLinkHandler(
const EntityLinkData &data,
const std::any &context) override;
diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp
index e285a92444..31080b4858 100644
--- a/Telegram/SourceFiles/facades.cpp
+++ b/Telegram/SourceFiles/facades.cpp
@@ -31,154 +31,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/media/history_view_media.h"
+#include "payments/payments_checkout_process.h"
#include "data/data_session.h"
#include "styles/style_chat.h"
-#include "webview/webview_embed.h"
-#include "webview/webview_interface.h"
-#include "core/local_url_handlers.h"
-#include "ui/widgets/window.h"
-#include "ui/toast/toast.h"
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QJsonValue>
-
-namespace Api {
-
-void GetPaymentForm(not_null<const HistoryItem*> 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<QEvent*> 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<Webview::Window>(
- window,
- window);
- if (!webview->widget()) {
- delete window;
- Ui::show(Box<InformBox>(
- 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<Main::Session*> session) {
@@ -266,12 +122,7 @@ void activateBotCommand(
} break;
case ButtonType::Buy: {
- if (Webview::Supported()) {
- Api::GetPaymentForm(msg);
- } else {
- Ui::show(Box<InformBox>(
- tr::lng_payments_not_supported(tr::now)));
- }
+ Payments::CheckoutProcess::Start(msg);
} break;
case ButtonType::Url: {
diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp
new file mode 100644
index 0000000000..916150a204
--- /dev/null
+++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp
@@ -0,0 +1,240 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "payments/payments_checkout_process.h"
+
+#include "payments/payments_form.h"
+#include "payments/ui/payments_panel.h"
+#include "payments/ui/payments_webview.h"
+#include "main/main_session.h"
+#include "main/main_account.h"
+#include "history/history_item.h"
+#include "history/history.h"
+#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
+#include "apiwrap.h"
+
+// #TODO payments errors
+#include "mainwindow.h"
+#include "ui/toast/toast.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+
+namespace Payments {
+namespace {
+
+struct SessionProcesses {
+ base::flat_map<FullMsgId, std::unique_ptr<CheckoutProcess>> map;
+ rpl::lifetime lifetime;
+};
+
+base::flat_map<not_null<Main::Session*>, SessionProcesses> Processes;
+
+[[nodiscard]] SessionProcesses &LookupSessionProcesses(
+ not_null<const HistoryItem*> 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<const HistoryItem*> 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<CheckoutProcess>(session, id, PrivateTag{})).first;
+ j->second->requestActivate();
+}
+
+CheckoutProcess::CheckoutProcess(
+ not_null<Main::Session*> session,
+ FullMsgId itemId,
+ PrivateTag)
+: _session(session)
+, _form(std::make_unique<Form>(session, itemId))
+, _panel(std::make_unique<Ui::Panel>(panelDelegate())) {
+ _form->updates(
+ ) | rpl::start_with_next([=](const FormUpdate &update) {
+ handleFormUpdate(update);
+ }, _lifetime);
+}
+
+CheckoutProcess::~CheckoutProcess() {
+}
+
+void CheckoutProcess::requestActivate() {
+ _panel->requestActivate();
+}
+
+not_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() {
+ return static_cast<PanelDelegate*>(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<Ui::WebviewWindow>(
+ 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<Ui::WebviewWindow>(
+ _form->details().url,
+ panelDelegate());
+ if (!_webviewWindow->shown()) {
+ // #TODO payments errors
+ }
+}
+
+void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) {
+ if (!message.isArray()) {
+ LOG(("Payments Error: "
+ "Not an array received in buy_callback arguments."));
+ return;
+ }
+ const auto list = message.array();
+ if (list.at(0).toString() != "payment_form_submit") {
+ return;
+ } else if (!list.at(1).isString()) {
+ LOG(("Payments Error: "
+ "Not a string received in buy_callback result."));
+ return;
+ }
+
+ auto error = QJsonParseError();
+ const auto document = QJsonDocument::fromJson(
+ list.at(1).toString().toUtf8(),
+ &error);
+ if (error.error != QJsonParseError::NoError) {
+ LOG(("Payments Error: "
+ "Failed to parse buy_callback arguments, error: %1."
+ ).arg(error.errorString()));
+ return;
+ } else if (!document.isObject()) {
+ LOG(("Payments Error: "
+ "Not an object decoded in buy_callback result."));
+ return;
+ }
+ const auto root = document.object();
+ const auto title = root.value("title").toString();
+ const auto credentials = root.value("credentials");
+ if (!credentials.isObject()) {
+ LOG(("Payments Error: "
+ "Not an object received in payment credentials."));
+ return;
+ }
+ const auto serializedCredentials = QJsonDocument(
+ credentials.toObject()
+ ).toJson(QJsonDocument::Compact);
+
+ _form->send(serializedCredentials);
+}
+
+bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {
+ if (Core::TryConvertUrlToLocal(uri) == uri) {
+ return true;
+ }
+ panelCloseSure();
+ App::wnd()->activate();
+ return false;
+}
+
+} // namespace Payments
diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h
new file mode 100644
index 0000000000..1eff37db96
--- /dev/null
+++ b/Telegram/SourceFiles/payments/payments_checkout_process.h
@@ -0,0 +1,69 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "payments/ui/payments_panel_delegate.h"
+#include "base/weak_ptr.h"
+
+class HistoryItem;
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Payments::Ui {
+class Panel;
+class WebviewWindow;
+} // namespace Payments::Ui
+
+namespace Payments {
+
+class Form;
+struct FormUpdate;
+struct FormError;
+struct SendError;
+
+class CheckoutProcess final
+ : public base::has_weak_ptr
+ , private Ui::PanelDelegate {
+ struct PrivateTag {};
+
+public:
+ static void Start(not_null<const HistoryItem*> item);
+
+ CheckoutProcess(
+ not_null<Main::Session*> session,
+ FullMsgId itemId,
+ PrivateTag);
+ ~CheckoutProcess();
+
+ void requestActivate();
+
+private:
+ [[nodiscard]] not_null<PanelDelegate*> 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<Main::Session*> _session;
+ const std::unique_ptr<Form> _form;
+ const std::unique_ptr<Ui::Panel> _panel;
+ std::unique_ptr<Ui::WebviewWindow> _webviewWindow;
+
+ rpl::lifetime _lifetime;
+
+};
+
+} // namespace Payments
diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp
new file mode 100644
index 0000000000..397a5058f1
--- /dev/null
+++ b/Telegram/SourceFiles/payments/payments_form.cpp
@@ -0,0 +1,154 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "payments/payments_form.h"
+
+#include "main/main_session.h"
+#include "data/data_session.h"
+#include "apiwrap.h"
+
+namespace Payments {
+namespace {
+
+[[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) {
+ return address.match([](const MTPDpostAddress &data) {
+ return Ui::Address{
+ .address1 = qs(data.vstreet_line1()),
+ .address2 = qs(data.vstreet_line2()),
+ .city = qs(data.vcity()),
+ .state = qs(data.vstate()),
+ .countryIso2 = qs(data.vcountry_iso2()),
+ .postCode = qs(data.vpost_code()),
+ };
+ });
+}
+
+} // namespace
+
+Form::Form(not_null<Main::Session*> session, FullMsgId itemId)
+: _session(session)
+, _msgId(itemId.msg) {
+ requestForm();
+}
+
+void Form::requestForm() {
+ _session->api().request(MTPpayments_GetPaymentForm(
+ MTP_int(_msgId)
+ )).done([=](const MTPpayments_PaymentForm &result) {
+ result.match([&](const auto &data) {
+ processForm(data);
+ });
+ }).fail([=](const MTP::Error &error) {
+ _updates.fire({ FormError{ error.type() } });
+ }).send();
+}
+
+void Form::processForm(const MTPDpayments_paymentForm &data) {
+ _session->data().processUsers(data.vusers());
+
+ data.vinvoice().match([&](const auto &data) {
+ processInvoice(data);
+ });
+ processDetails(data);
+ if (const auto info = data.vsaved_info()) {
+ info->match([&](const auto &data) {
+ processSavedInformation(data);
+ });
+ }
+ if (const auto credentials = data.vsaved_credentials()) {
+ credentials->match([&](const auto &data) {
+ processSavedCredentials(data);
+ });
+ }
+
+ _updates.fire({ FormReady{} });
+}
+
+void Form::processInvoice(const MTPDinvoice &data) {
+ auto &&prices = ranges::views::all(
+ data.vprices().v
+ ) | ranges::views::transform([](const MTPLabeledPrice &price) {
+ return price.match([&](const MTPDlabeledPrice &data) {
+ return Ui::LabeledPrice{
+ .label = qs(data.vlabel()),
+ .price = data.vamount().v,
+ };
+ });
+ });
+ _invoice = Ui::Invoice{
+ .prices = prices | ranges::to_vector,
+ .currency = qs(data.vcurrency()),
+
+ .isNameRequested = data.is_name_requested(),
+ .isPhoneRequested = data.is_phone_requested(),
+ .isEmailRequested = data.is_email_requested(),
+ .isShippingAddressRequested = data.is_shipping_address_requested(),
+ .isFlexible = data.is_flexible(),
+ .isTest = data.is_test(),
+
+ .phoneSentToProvider = data.is_phone_to_provider(),
+ .emailSentToProvider = data.is_email_to_provider(),
+ };
+}
+
+void Form::processDetails(const MTPDpayments_paymentForm &data) {
+ _session->data().processUsers(data.vusers());
+ const auto nativeParams = data.vnative_params();
+ auto nativeParamsJson = nativeParams
+ ? nativeParams->match(
+ [&](const MTPDdataJSON &data) { return data.vdata().v; })
+ : QByteArray();
+ _details = FormDetails{
+ .url = qs(data.vurl()),
+ .nativeProvider = qs(data.vnative_provider().value_or_empty()),
+ .nativeParamsJson = std::move(nativeParamsJson),
+ .botId = data.vbot_id().v,
+ .providerId = data.vprovider_id().v,
+ .canSaveCredentials = data.is_can_save_credentials(),
+ .passwordMissing = data.is_password_missing(),
+ };
+}
+
+void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
+ const auto address = data.vshipping_address();
+ _savedInformation = Ui::SavedInformation{
+ .name = qs(data.vname().value_or_empty()),
+ .phone = qs(data.vphone().value_or_empty()),
+ .email = qs(data.vemail().value_or_empty()),
+ .shippingAddress = address ? ParseAddress(*address) : Ui::Address(),
+ };
+}
+
+void Form::processSavedCredentials(
+ const MTPDpaymentSavedCredentialsCard &data) {
+ _savedCredentials = Ui::SavedCredentials{
+ .id = qs(data.vid()),
+ .title = qs(data.vtitle()),
+ };
+}
+
+void Form::send(const QByteArray &serializedCredentials) {
+ _session->api().request(MTPpayments_SendPaymentForm(
+ MTP_flags(0),
+ MTP_int(_msgId),
+ MTPstring(), // requested_info_id
+ MTPstring(), // shipping_option_id,
+ MTP_inputPaymentCredentials(
+ MTP_flags(0),
+ MTP_dataJSON(MTP_bytes(serializedCredentials)))
+ )).done([=](const MTPpayments_PaymentResult &result) {
+ result.match([&](const MTPDpayments_paymentResult &data) {
+ _updates.fire({ PaymentFinished{ data.vupdates() } });
+ }, [&](const MTPDpayments_paymentVerificationNeeded &data) {
+ _updates.fire({ VerificationNeeded{ qs(data.vurl()) } });
+ });
+ }).fail([=](const MTP::Error &error) {
+ _updates.fire({ SendError{ error.type() } });
+ }).send();
+}
+
+} // namespace Payments
diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h
new file mode 100644
index 0000000000..92288605ce
--- /dev/null
+++ b/Telegram/SourceFiles/payments/payments_form.h
@@ -0,0 +1,106 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "payments/ui/payments_panel_data.h"
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Payments {
+
+struct FormDetails {
+ QString url;
+ QString nativeProvider;
+ QByteArray nativeParamsJson;
+ UserId botId = 0;
+ UserId providerId = 0;
+ bool canSaveCredentials = false;
+ bool passwordMissing = false;
+
+ [[nodiscard]] bool valid() const {
+ return !url.isEmpty();
+ }
+ [[nodiscard]] explicit operator bool() const {
+ return valid();
+ }
+};
+
+struct FormReady {};
+
+struct FormError {
+ QString type;
+};
+
+struct SendError {
+ QString type;
+};
+
+struct VerificationNeeded {
+ QString url;
+};
+
+struct PaymentFinished {
+ MTPUpdates updates;
+};
+
+struct FormUpdate {
+ std::variant<
+ FormReady,
+ FormError,
+ SendError,
+ VerificationNeeded,
+ PaymentFinished> data;
+};
+
+class Form final {
+public:
+ Form(not_null<Main::Session*> 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<FormUpdate> 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<Main::Session*> _session;
+ MsgId _msgId = 0;
+
+ Ui::Invoice _invoice;
+ FormDetails _details;
+ Ui::SavedInformation _savedInformation;
+ Ui::SavedCredentials _savedCredentials;
+
+ rpl::event_stream<FormUpdate> _updates;
+
+};
+
+} // namespace Payments
diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style
new file mode 100644
index 0000000000..6e99489ebd
--- /dev/null
+++ b/Telegram/SourceFiles/payments/ui/payments.style
@@ -0,0 +1,10 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+using "ui/basic.style";
+
+using "passport/passport.style";
diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
new file mode 100644
index 0000000000..935aa7cdd7
--- /dev/null
+++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
@@ -0,0 +1,87 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "payments/ui/payments_form_summary.h"
+
+#include "payments/ui/payments_panel_delegate.h"
+#include "ui/widgets/scroll_area.h"
+#include "ui/widgets/buttons.h"
+#include "ui/wrap/vertical_layout.h"
+#include "ui/wrap/fade_wrap.h"
+#include "lang/lang_keys.h"
+#include "styles/style_payments.h"
+#include "styles/style_passport.h"
+
+namespace Payments::Ui {
+
+using namespace ::Ui;
+
+class PanelDelegate;
+
+FormSummary::FormSummary(
+ QWidget *parent,
+ const Invoice &invoice,
+ not_null<PanelDelegate*> 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;<