Start proper payments processing.

This commit is contained in:
John Preston 2021-03-23 16:34:34 +04:00
parent 25bbde2739
commit 4c707bff29
27 changed files with 1110 additions and 179 deletions

View File

@ -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

View File

@ -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...";

View File

@ -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) {

View File

@ -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;

View File

@ -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: {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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";

View File

@ -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;
_topShadow->toggleOn(
_scroll->scrollTopValue() | rpl::map(_1 > 0));
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
inner->heightValue(),
_1 + _2 < _3));
}
not_null<Ui::RpWidget*> FormSummary::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(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

View File

@ -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<PanelDelegate*> delegate);
private:
void resizeEvent(QResizeEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
const not_null<PanelDelegate*> _delegate;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
};
} // namespace Payments::Ui

View File

@ -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<PanelDelegate*> delegate)
: _delegate(delegate)
, _widget(std::make_unique<SeparatePanel>()) {
_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<FormSummary>(_widget.get(), invoice, _delegate));
}
} // namespace Payments::Ui

View File

@ -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<PanelDelegate*> delegate);
~Panel();
void requestActivate();
void showForm(const Invoice &invoice);
private:
const not_null<PanelDelegate*> _delegate;
std::unique_ptr<SeparatePanel> _widget;
};
} // namespace Payments::Ui

View File

@ -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<LabeledPrice> 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

View File

@ -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

View File

@ -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<PanelDelegate*> 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<QEvent*> 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<Webview::Window>(
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

View File

@ -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<PanelDelegate*> delegate);
[[nodiscard]] bool shown() const;
void navigate(const QString &url);
private:
Ui::Window _window;
Webview::Window *_webview = nullptr;
};
} // namespace Payments::Ui

View File

@ -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 <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtWidgets/QApplication>
#include <QtWidgets/QDesktopWidget>
@ -35,7 +33,7 @@ SeparatePanel::SeparatePanel()
, _back(this, object_ptr<Ui::IconButton>(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;

View File

@ -22,7 +22,7 @@ class FadeWrapScaled;
namespace Ui {
class SeparatePanel : public Ui::RpWidget, private base::Subscriber {
class SeparatePanel final : public Ui::RpWidget {
public:
SeparatePanel();

@ -1 +1 @@
Subproject commit eded7cc540123eaf26361958b9a61c65cb2f7cfc
Subproject commit 65498491465fa64ffdf96a2b7cdeb67bfb697d5b

View File

@ -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
)

@ -1 +1 @@
Subproject commit 8f0c0164cdce6bcbc7bcfe531963e2a552f6290d
Subproject commit 7a5fd82692d2fb5df9b48c08c354f4400157a999

@ -1 +1 @@
Subproject commit 404c83d77e5edb8a39f8e9f56a6340960fe5070e
Subproject commit 45faed44e7f4d11fec79b7a70e4a35dc91ef3fdb

@ -1 +1 @@
Subproject commit 99089134e34c19e4c6fdb25569ade0d6f081bdb1
Subproject commit 8686905ee40eb8dbe171024e04e41a32069c8add

@ -1 +1 @@
Subproject commit f95214cbe4b0a31ac989e0aceb8cc4f63c1322e6
Subproject commit 5270a1dbbdbee643e187e175f798595b4bc49996

@ -1 +1 @@
Subproject commit 7491d160231a18dec6aec1f3c1e1575382d10745
Subproject commit 49887261a55665f6e195049bcc22b6495a44cc36