Compare commits

...

27 Commits

Author SHA1 Message Date
John Preston
672aacd528 wip linux webview 2021-03-31 22:20:30 +04:00
John Preston
1a5e807fa9 Fix build for macOS / Linux. 2021-03-31 21:38:32 +04:00
John Preston
f98f4f0d14 Simple receipt viewing. 2021-03-30 10:03:54 +04:00
John Preston
78def16ced Fix showing comments from the beginning. 2021-03-29 19:53:55 +04:00
John Preston
cd7b3419de Add phone format and validation in payments. 2021-03-29 18:56:26 +04:00
John Preston
dc4048f1c1 Add local validation for card information. 2021-03-29 16:16:54 +04:00
John Preston
bef5320163 Add nice country choosing in payments. 2021-03-26 21:09:09 +04:00
John Preston
9a722ea8d4 Improve checkout information / card page design. 2021-03-26 19:23:12 +04:00
John Preston
1aefada45d Improve checkout main page design. 2021-03-26 17:05:31 +04:00
John Preston
f5a8bf0e74 Handle native / non-native payment methods (same way). 2021-03-25 23:22:45 +04:00
John Preston
738439c334 Support entering card details natively. 2021-03-25 20:56:20 +04:00
John Preston
8949c9969b Fix jumping of Media Viewer in some DEs. 2021-03-25 19:10:01 +04:00
John Preston
cdf6fb1512 Port required parts of Stripe SDK to lib_stripe. 2021-03-24 22:00:49 +04:00
John Preston
fb0ea59ff3 Validate saved information on payment form open. 2021-03-24 16:41:46 +04:00
John Preston
36f5be60f4 Show some payment errors, focus fields. 2021-03-24 15:30:01 +04:00
John Preston
46508f7e5e First full-featured version of payments, no design. 2021-03-23 22:30:00 +04:00
John Preston
28137dfb60 Start proper payments processing. 2021-03-23 16:34:34 +04:00
John Preston
e7784620d3 Use navigation cancel in Webview. 2021-03-22 22:56:29 +04:00
John Preston
462986e9c3 Update lib_webview. 2021-03-22 22:56:29 +04:00
John Preston
c11de2380e Start Linux support. 2021-03-22 22:56:27 +04:00
John Preston
a432e826a6 Use lib_webview implementation on macOS. 2021-03-22 22:54:54 +04:00
John Preston
ea9e85e70f Use lib_webview implementation on Windows. 2021-03-22 22:54:54 +04:00
John Preston
d3829c52ec Fix webview on macOS. 2021-03-22 22:54:54 +04:00
John Preston
9f04570335 3DSecure in Proof-Of-Concept payments. 2021-03-22 22:54:53 +04:00
John Preston
3c486522a7 Proof-Of-Concept simplest invoice payment. 2021-03-22 22:54:53 +04:00
John Preston
07cd8c4e83 Link Telegram with lib_webview. 2021-03-22 22:54:53 +04:00
John Preston
7447c6ea75 Add webview / lib_webview submodules. 2021-03-22 22:54:53 +04:00
114 changed files with 6418 additions and 873 deletions

3
.gitmodules vendored
View File

@@ -88,3 +88,6 @@
[submodule "Telegram/ThirdParty/tgcalls"]
path = Telegram/ThirdParty/tgcalls
url = https://github.com/TelegramMessenger/tgcalls.git
[submodule "Telegram/lib_webview"]
path = Telegram/lib_webview
url = https://github.com/desktop-app/lib_webview.git

View File

@@ -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)
@@ -26,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)
@@ -55,7 +57,9 @@ PRIVATE
desktop-app::lib_storage
desktop-app::lib_lottie
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
@@ -70,7 +74,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
@@ -249,8 +258,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
@@ -383,8 +390,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
@@ -796,8 +801,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
@@ -808,6 +811,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
@@ -1007,8 +1014,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
@@ -1024,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

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1860,6 +1860,36 @@ 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_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...";
"lng_payments_shipping_address" = "Shipping Address";
"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_info_name" = "Name";
"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";
"lng_call_status_incoming" = "is calling you...";
"lng_call_status_connecting" = "connecting...";
"lng_call_status_exchanging" = "exchanging encryption keys...";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -1820,7 +1820,7 @@ Utf8String FormatDateTime(
).toUtf8();
}
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String &currency) {
Utf8String FormatMoneyAmount(int64 amount, const Utf8String &currency) {
return Ui::FillAmountAndCurrency(
amount,
QString::fromUtf8(currency)).toUtf8();

View File

@@ -660,7 +660,7 @@ Utf8String FormatDateTime(
QChar dateSeparator = QChar('.'),
QChar timeSeparator = QChar(':'),
QChar separator = QChar(' '));
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String &currency);
Utf8String FormatMoneyAmount(int64 amount, const Utf8String &currency);
Utf8String FormatFileSize(int64 size);
Utf8String FormatDuration(int64 seconds);

View File

@@ -31,6 +31,7 @@ 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"
@@ -121,7 +122,7 @@ void activateBotCommand(
} break;
case ButtonType::Buy: {
Ui::show(Box<InformBox>(tr::lng_payments_not_supported(tr::now)));
Payments::CheckoutProcess::Start(msg);
} break;
case ButtonType::Url: {
@@ -260,7 +261,7 @@ void showChatsList(not_null<Main::Session*> session) {
if (const auto m = CheckMainWidget(session)) {
m->ui_showPeerHistory(
0,
Window::SectionShow::Way::ClearStack,
::Window::SectionShow::Way::ClearStack,
0);
}
}
@@ -277,7 +278,7 @@ void showPeerHistory(not_null<const PeerData*> peer, MsgId msgId) {
if (const auto m = CheckMainWidget(&peer->session())) {
m->ui_showPeerHistory(
peer->id,
Window::SectionShow::Way::ClearStack,
::Window::SectionShow::Way::ClearStack,
msgId);
}
}

View File

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

View File

@@ -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<HistoryServicePayment>()->amount = Ui::FillAmountAndCurrency(amount, currency);
Get<HistoryServicePayment>()->amount = Ui::FillAmountAndCurrency(
amount,
currency);
} else if (message.vaction().type() == mtpc_messageActionGroupCall) {
const auto &data = message.vaction().c_messageActionGroupCall();
if (data.vduration()) {

View File

@@ -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<QKeyEvent*> 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(); });
@@ -61,8 +73,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 +263,7 @@ QString PhoneWidget::fullNumber() const {
}
void PhoneWidget::selectCountry(const QString &country) {
_country->onChooseCountry(country);
_country->chooseCountry(country);
}
void PhoneWidget::setInnerFocus() {

View File

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

View File

@@ -101,7 +101,7 @@ void Panel::showBox(
}
void Panel::showToast(const QString &text) {
_widget->showToast(text);
_widget->showToast({ text });
}
Panel::~Panel() = default;

View File

@@ -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<Row>{
{
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,

View File

@@ -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"
@@ -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<Ui::MaskedInputField>(

View File

@@ -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<Ui::RpWidget*> 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<Ui::BoxContent> 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<PanelDetailsRow*> PanelEditDocument::findRow(
not_null<Ui::PanelDetailsRow*> 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<PanelDetailsRow>();
auto first = QPointer<Ui::PanelDetailsRow>();
for (const auto &[i, field] : ranges::views::reverse(_details)) {
const auto &row = _scheme.rows[i];
if (row.valueClass == Scheme::ValueClass::Additional

View File

@@ -24,15 +24,19 @@ template <typename Widget>
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<std::optional<QString>(const QString &value)>;
using Formatter = Fn<QString(const QString &value)>;
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<PanelDetailsRow*> findRow(const QString &key) const;
not_null<Ui::PanelDetailsRow*> findRow(const QString &key) const;
not_null<PanelController*> _controller;
Scheme _scheme;
@@ -151,7 +155,7 @@ private:
QPointer<EditScans> _editScans;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
std::map<int, QPointer<PanelDetailsRow>> _details;
std::map<int, QPointer<Ui::PanelDetailsRow>> _details;
bool _fieldsChanged = false;
bool _additionalShown = false;

View File

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

View File

@@ -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<PanelController*> controller)

View File

@@ -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<Ui::RpWidget*> setupContent();

View File

@@ -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 <QtCore/QRegularExpression>
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<PanelController*> controller,
Fn<void(object_ptr<BoxContent>)> 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<PanelController*> _controller;
object_ptr<Ui::LinkButton> _link;
QString _defaultCountry;
Fn<void(object_ptr<BoxContent>)> _showBox;
object_ptr<LinkButton> _link;
rpl::variable<QString> _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<DateInput> &field) const;
object_ptr<DateInput> _day;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator1;
object_ptr<PaddingWrap<FlatLabel>> _separator1;
object_ptr<DateInput> _month;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator2;
object_ptr<PaddingWrap<FlatLabel>> _separator2;
object_ptr<DateInput> _year;
rpl::variable<QString> _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<Ui::AbstractCheckView> createRadioView(
Ui::RadioView* &weak) const;
std::unique_ptr<AbstractCheckView> createRadioView(
RadioView* &weak) const;
std::shared_ptr<Ui::RadioenumGroup<Gender>> _group;
Ui::RadioView *_maleRadio = nullptr;
Ui::RadioView *_femaleRadio = nullptr;
object_ptr<Ui::Radioenum<Gender>> _male;
object_ptr<Ui::Radioenum<Gender>> _female;
std::shared_ptr<RadioenumGroup<Gender>> _group;
RadioView *_maleRadio = nullptr;
RadioView *_femaleRadio = nullptr;
object_ptr<Radioenum<Gender>> _male;
object_ptr<Radioenum<Gender>> _female;
rpl::variable<QString> _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<PanelController*> controller,
Fn<void(object_ptr<BoxContent>)> 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<CountrySelectBox>(!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<CountrySelectBox>(
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<Ui::FlatLabel>(
object_ptr<FlatLabel>(
this,
QString(" / "),
st::passportDetailsSeparator),
@@ -540,7 +547,7 @@ DateRow::DateRow(
GetMonth(value))
, _separator2(
this,
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
this,
QString(" / "),
st::passportDetailsSeparator),
@@ -552,7 +559,7 @@ DateRow::DateRow(
GetYear(value))
, _value(valueCurrent()) {
const auto focused = [=](const object_ptr<DateInput> &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<Ui::RadioenumGroup<Gender>>(*StringToGender(value))
: std::make_shared<Ui::RadioenumGroup<Gender>>())
? std::make_shared<RadioenumGroup<Gender>>(*StringToGender(value))
: std::make_shared<RadioenumGroup<Gender>>())
, _male(
this,
_group,
@@ -868,9 +875,9 @@ GenderRow::GenderRow(
});
}
std::unique_ptr<Ui::AbstractCheckView> GenderRow::createRadioView(
Ui::RadioView* &weak) const {
auto result = std::make_unique<Ui::RadioView>(st::defaultRadio, false);
std::unique_ptr<AbstractCheckView> GenderRow::createRadioView(
RadioView* &weak) const {
auto result = std::make_unique<RadioView>(st::defaultRadio, false);
weak = result.get();
return result;
}
@@ -959,8 +966,9 @@ PanelDetailsRow::PanelDetailsRow(
object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
QWidget *parent,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
Type type,
not_null<PanelController*> controller,
const QString &label,
int maxLabelWidth,
const QString &value,
@@ -969,7 +977,7 @@ object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
auto result = [&]() -> object_ptr<PanelDetailsRow> {
switch (type) {
case Type::Text:
return object_ptr<AbstractTextRow<Ui::InputField>>(
return object_ptr<AbstractTextRow<InputField>>(
parent,
label,
maxLabelWidth,
@@ -985,7 +993,8 @@ object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
case Type::Country:
return object_ptr<CountryRow>(
parent,
controller,
showBox,
defaultCountry,
label,
maxLabelWidth,
value);
@@ -1062,7 +1071,7 @@ void PanelDetailsRow::showError(std::optional<QString> error) {
if (!_error) {
_error.create(
this,
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
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

View File

@@ -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 <typename Widget>
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<PanelDetailsRow> Create(
QWidget *parent,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
Type type,
not_null<PanelController*> controller,
const QString &label,
int maxLabelWidth,
const QString &value,
@@ -74,11 +75,11 @@ private:
QString _label;
int _maxLabelWidth = 0;
object_ptr<Ui::SlideWrap<Ui::FlatLabel>> _error = { nullptr };
object_ptr<SlideWrap<FlatLabel>> _error = { nullptr };
bool _errorShown = false;
bool _errorHideSubscription = false;
Ui::Animations::Simple _errorAnimation;
Animations::Simple _errorAnimation;
};
} // namespace Passport
} // namespace Passport::Ui

View File

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

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

View File

@@ -0,0 +1,462 @@
/*
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 "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 "data/data_user.h" // UserData::isBot.
#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
#include "core/file_utilities.h" // File::OpenUrl.
#include "apiwrap.h"
// #TODO payments errors
#include "mainwindow.h"
#include "ui/toasts/common_toasts.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);
_panel->backRequests(
) | rpl::start_with_next([=] {
showForm();
}, _panel->lifetime());
showForm();
}
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, [&](const FormReady &) {
performInitialSilentValidation();
if (!_initialSilentValidation) {
showForm();
}
}, [&](const ThumbnailUpdated &data) {
_panel->updateFormThumbnail(data.thumbnail);
}, [&](const ValidateFinished &) {
if (_initialSilentValidation) {
_initialSilentValidation = false;
}
showForm();
if (_submitState == SubmitState::Validation) {
_submitState = SubmitState::Validated;
panelSubmit();
}
}, [&](const PaymentMethodUpdate &) {
showForm();
}, [&](const VerificationNeeded &data) {
if (!_panel->showWebview(data.url, false)) {
File::OpenUrl(data.url);
panelCloseSure();
}
}, [&](const PaymentFinished &data) {
const auto weak = base::make_weak(this);
_session->api().applyUpdates(data.updates);
if (weak) {
panelCloseSure();
}
}, [&](const Error &error) {
handleError(error);
});
}
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 (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 (_initialSilentValidation) {
_initialSilentValidation = false;
showForm();
return;
}
using InfoField = Ui::InformationField;
using CardField = Ui::CardField;
if (id == u"REQ_INFO_NAME_INVALID"_q) {
showInformationError(InfoField::Name);
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
showInformationError(InfoField::Email);
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
showInformationError(InfoField::Phone);
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
showInformationError(InfoField::ShippingStreet);
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
showInformationError(InfoField::ShippingCity);
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
showInformationError(InfoField::ShippingState);
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
showInformationError(InfoField::ShippingCountry);
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
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) {
showToast({ "Error: Shipping to the selected country is not available!" }); // #TODO payments errors message
} else {
showToast({ "Error: " + id });
}
} 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 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 });
}
} 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"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.");
}
}
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() {
if (_submitState == SubmitState::Validation
|| _submitState == SubmitState::Finishing) {
return;
}
const auto &method = _form->paymentMethod();
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;
} else if (!method.newCredentials && !method.savedCredentials) {
editPaymentMethod();
return;
}
_submitState = SubmitState::Finishing;
_form->submit();
}
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;
}
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;
}
crl::on_main(this, [=] {
panelCloseSure();
App::wnd()->activate();
});
return false;
}
void CheckoutProcess::panelEditPaymentMethod() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
editPaymentMethod();
}
void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) {
_form->validateCard(data);
}
void CheckoutProcess::panelEditShippingInformation() {
showEditInformation(Ui::InformationField::ShippingStreet);
}
void CheckoutProcess::panelEditName() {
showEditInformation(Ui::InformationField::Name);
}
void CheckoutProcess::panelEditEmail() {
showEditInformation(Ui::InformationField::Email);
}
void CheckoutProcess::panelEditPhone() {
showEditInformation(Ui::InformationField::Phone);
}
void CheckoutProcess::showForm() {
_panel->showForm(
_form->invoice(),
_form->savedInformation(),
_form->paymentMethod().ui,
_form->shippingOptions());
}
void CheckoutProcess::showEditInformation(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showEditInformation(
_form->invoice(),
_form->savedInformation(),
field);
}
void CheckoutProcess::showInformationError(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showInformationError(
_form->invoice(),
_form->savedInformation(),
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());
}
void CheckoutProcess::editPaymentMethod() {
_panel->choosePaymentMethod(_form->paymentMethod().ui);
}
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<Ui::BoxContent> box) {
_panel->showBox(std::move(box));
}
void CheckoutProcess::performInitialSilentValidation() {
const auto &invoice = _form->invoice();
const auto &saved = _form->savedInformation();
if (invoice.receipt
|| (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::panelWebviewDataPath() {
return _session->domain().local().webviewDataPath();
}
} // namespace Payments

View File

@@ -0,0 +1,98 @@
/*
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;
enum class InformationField;
enum class CardField;
} // namespace Payments::Ui
namespace Payments {
class Form;
struct FormUpdate;
struct Error;
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:
enum class SubmitState {
None,
Validation,
Validated,
Finishing,
};
[[nodiscard]] not_null<PanelDelegate*> panelDelegate();
void handleFormUpdate(const FormUpdate &update);
void handleError(const Error &error);
void showForm();
void showEditInformation(Ui::InformationField field);
void showInformationError(Ui::InformationField field);
void showCardError(Ui::CardField field);
void chooseShippingOption();
void editPaymentMethod();
void performInitialSilentValidation();
void panelRequestClose() override;
void panelCloseSure() override;
void panelSubmit() override;
void panelWebviewMessage(const QJsonDocument &message) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelEditPaymentMethod() 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 panelValidateCard(Ui::UncheckedCardDetails data) override;
void panelShowBox(object_ptr<Ui::BoxContent> box) override;
QString panelWebviewDataPath() override;
const not_null<Main::Session*> _session;
const std::unique_ptr<Form> _form;
const std::unique_ptr<Ui::Panel> _panel;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;
rpl::lifetime _lifetime;
};
} // namespace Payments

View File

@@ -0,0 +1,669 @@
/*
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 "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 "data/data_countries.h"
#include "history/history_item.h"
#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.
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
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()),
};
});
}
[[nodiscard]] std::vector<Ui::LabeledPrice> ParsePrices(
const MTPVector<MTPLabeledPrice> &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<const int64*>(&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)));
}
[[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<Main::Session*> session, FullMsgId itemId)
: _session(session)
, _api(&_session->mtp())
, _msgId(itemId) {
fillInvoiceFromMessage();
if (_receiptMsgId) {
requestReceipt();
} else {
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()) {
_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);
}
}
}
}
}
void Form::loadThumbnail(not_null<PhotoData*> 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<ThumbnailLoadProcess>();
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<Data::PhotoMedia> &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<Data::PhotoMedia> &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<const Image*> 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.msg)
)).done([=](const MTPpayments_PaymentForm &result) {
result.match([&](const auto &data) {
processForm(data);
});
}).fail([=](const MTP::Error &error) {
_updates.fire(Error{ Error::Type::Form, error.type() });
}).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());
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);
});
}
fillPaymentMethodInformation();
_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),
.prices = ParsePrices(data.vprices()),
.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) {
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(),
};
if (_details.botId) {
if (const auto bot = _session->data().userLoaded(_details.botId)) {
_invoice.cover.seller = bot->name;
}
}
}
void Form::processDetails(const MTPDpayments_paymentReceipt &data) {
_invoice.receipt = Ui::Receipt{
.date = data.vdate().v,
.totalAmount = *reinterpret_cast<const int64*>(
&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{
.defaultPhone = defaultPhone(),
.defaultCountry = defaultCountry(),
.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) {
// #TODO payments save
//_nativePayment.savedCredentials = SavedCredentials{
// .id = qs(data.vid()),
// .title = qs(data.vtitle()),
//};
refreshPaymentMethodDetails();
}
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;
_paymentMethod.ui.native.defaultCountry = defaultCountry();
}
QString Form::defaultPhone() const {
return _session->user()->phone();
}
QString Form::defaultCountry() const {
return Data::CountryISO2ByPhone(defaultPhone());
}
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,
&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;
}
_paymentMethod.native = NativePaymentMethod{
.data = StripePaymentMethod{
.publishableKey = key,
},
};
_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::submit() {
Expects(!_paymentMethod.newCredentials.data.isEmpty()); // #TODO payments save
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.msg),
MTP_string(_requestedInformationId),
MTP_string(_shippingOptions.selectedId),
MTP_inputPaymentCredentials(
MTP_flags(0),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))
)).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(Error{ Error::Type::Send, error.type() });
}).send();
}
void Form::validateInformation(const Ui::RequestedInformation &information) {
if (_validateRequestId) {
if (_validatedInformation == information) {
return;
}
_api.request(base::take(_validateRequestId)).cancel();
}
_validatedInformation = information;
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),
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(Error{ Error::Type::Validate, error.type() });
}).send();
}
bool Form::validateInformationLocal(
const Ui::RequestedInformation &information) const {
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) {
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();
}
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<StripePaymentMethod>(&native)) {
validateCard(*stripe, details);
} else {
Unexpected("Native payment provider in Form::validateCard.");
}
}
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) {
Expects(!method.publishableKey.isEmpty());
if (_stripe) {
return;
}
auto configuration = Stripe::PaymentConfiguration{
.publishableKey = method.publishableKey,
.companyName = "Telegram",
};
_stripe = std::make_unique<Stripe::APIClient>(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());
_paymentMethod.newCredentials = credentials;
refreshPaymentMethodDetails();
_updates.fire(PaymentMethodUpdate{});
}
void Form::setShippingOption(const QString &id) {
_shippingOptions.selectedId = id;
}
void Form::processShippingOptions(const QVector<MTPShippingOption> &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

View File

@@ -0,0 +1,243 @@
/*
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"
#include "base/weak_ptr.h"
#include "mtproto/sender.h"
class Image;
namespace Stripe {
class APIClient;
} // namespace Stripe
namespace Main {
class Session;
} // namespace Main
namespace Data {
class PhotoMedia;
} // namespace Data
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 ThumbnailLoadProcess {
std::shared_ptr<Data::PhotoMedia> view;
bool blurredSet = false;
rpl::lifetime lifetime;
};
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 StripePaymentMethod {
QString publishableKey;
};
struct NativePaymentMethod {
std::variant<
v::null_t,
StripePaymentMethod> data;
[[nodiscard]] bool valid() const {
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 ThumbnailUpdated {
QImage thumbnail;
};
struct ValidateFinished {};
struct PaymentMethodUpdate {};
struct VerificationNeeded {
QString url;
};
struct PaymentFinished {
MTPUpdates updates;
};
struct Error {
enum class Type {
None,
Form,
Validate,
Stripe,
Send,
};
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<
FormReady,
ThumbnailUpdated,
ValidateFinished,
PaymentMethodUpdate,
VerificationNeeded,
PaymentFinished,
Error> {
using variant::variant;
};
class Form final : public base::has_weak_ptr {
public:
Form(not_null<Main::Session*> session, FullMsgId itemId);
~Form();
[[nodiscard]] const Ui::Invoice &invoice() const {
return _invoice;
}
[[nodiscard]] const FormDetails &details() const {
return _details;
}
[[nodiscard]] const Ui::RequestedInformation &savedInformation() const {
return _savedInformation;
}
[[nodiscard]] const PaymentMethod &paymentMethod() const {
return _paymentMethod;
}
[[nodiscard]] const Ui::ShippingOptions &shippingOptions() const {
return _shippingOptions;
}
[[nodiscard]] rpl::producer<FormUpdate> updates() const {
return _updates.events();
}
void validateInformation(const Ui::RequestedInformation &information);
void validateCard(const Ui::UncheckedCardDetails &details);
void setPaymentCredentials(const NewCredentials &credentials);
void setShippingOption(const QString &id);
void submit();
private:
void fillInvoiceFromMessage();
void loadThumbnail(not_null<PhotoData*> photo);
[[nodiscard]] QImage prepareGoodThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareBlurredThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareThumbnail(
not_null<const Image*> image,
bool blurred = false) const;
[[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);
void processShippingOptions(const QVector<MTPShippingOption> &data);
void fillPaymentMethodInformation();
void fillStripeNativeMethod();
void refreshPaymentMethodDetails();
[[nodiscard]] QString defaultPhone() const;
[[nodiscard]] QString defaultCountry() const;
void validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details);
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<Main::Session*> _session;
MTP::Sender _api;
FullMsgId _msgId;
FullMsgId _receiptMsgId;
Ui::Invoice _invoice;
std::unique_ptr<ThumbnailLoadProcess> _thumbnailLoadProcess;
FormDetails _details;
Ui::RequestedInformation _savedInformation;
PaymentMethod _paymentMethod;
Ui::RequestedInformation _validatedInformation;
mtpRequestId _validateRequestId = 0;
std::unique_ptr<Stripe::APIClient> _stripe;
Ui::ShippingOptions _shippingOptions;
QString _requestedInformationId;
rpl::event_stream<FormUpdate> _updates;
};
} // namespace Payments

View File

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

View File

@@ -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 <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
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(MakeEncodable(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<QNetworkReply> 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

View File

@@ -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 <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
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<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace Stripe

View File

@@ -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 <functional>
namespace Stripe {
class Error;
class Token;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace Stripe

View File

@@ -0,0 +1,188 @@
/*
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 {
namespace {
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;
}
}
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;
}
}
} // 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",
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").toLower());
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;
}
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();
}
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

View File

@@ -0,0 +1,99 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace Stripe {
enum class CardBrand {
Visa,
Amex,
MasterCard,
Discover,
JCB,
DinersClub,
UnionPay,
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]] 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();
}
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

View File

@@ -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() {
return "card";
}
std::map<QString, QString> 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

View File

@@ -0,0 +1,32 @@
/*
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 {
struct CardParams {
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;
[[nodiscard]] static QString rootObjectName();
[[nodiscard]] std::map<QString, QString> formFieldValues() const;
};
} // namespace Stripe

View File

@@ -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 <QtCore/QDate>
namespace Stripe {
namespace {
constexpr auto kMinCvcLength = 3;
struct BinRange {
QString low;
QString high;
int length = 0;
CardBrand brand = CardBrand::Unknown;
};
[[nodiscard]] const std::vector<BinRange> &AllRanges() {
static auto kResult = std::vector<BinRange>{
// 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<BinRange> BinRangesForNumber(
const QString &sanitized) {
const auto &all = AllRanges();
auto result = std::vector<BinRange>();
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<CardBrand> PossibleBrandsForNumber(
const QString &sanitized) {
const auto ranges = BinRangesForNumber(sanitized);
auto result = std::vector<CardBrand>();
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<int> 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

View File

@@ -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<int> CardNumberFormat(const QString &number);
} // namespace Stripe

View File

@@ -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<QStringView> keys) {
for (const auto &key : keys) {
if (object.value(key).isUndefined()) {
return false;
}
}
return true;
}
} // namespace Stripe

View File

@@ -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 <QtCore/QJsonObject>
namespace Stripe {
[[nodiscard]] bool ContainsFields(
const QJsonObject &object,
std::vector<QStringView> keys);
} // namespace Stripe

View File

@@ -0,0 +1,107 @@
/*
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::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()) {
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);
}
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace Stripe

View File

@@ -0,0 +1,66 @@
/*
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 <QtCore/QString>
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 &parameter = QString())
: _code(code)
, _description(description)
, _message(message)
, _parameter(parameter) {
}
[[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;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Code _code = Code::None;
QString _description;
QString _message;
QString _parameter;
};
} // namespace Stripe

View File

@@ -0,0 +1,39 @@
/*
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 <QtCore/QString>
#include <map>
namespace Stripe {
class FormEncodable {
public:
[[nodiscard]] virtual QString rootObjectName() = 0;
[[nodiscard]] virtual std::map<QString, QString> formFieldValues() = 0;
};
template <typename T>
struct MakeEncodable final : FormEncodable {
public:
MakeEncodable(const T &value) : _value(value) {
}
QString rootObjectName() override {
return _value.rootObjectName();
}
std::map<QString, QString> formFieldValues() override {
return _value.formFieldValues();
}
private:
const T &_value;
};
} // namespace Stripe

View File

@@ -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 <QStringList>
#include <QUrl>
namespace Stripe {
QByteArray FormEncoder::formEncodedDataForObject(
FormEncodable &&object) {
const auto root = object.rootObjectName();
const auto values = object.formFieldValues();
auto result = QByteArray();
auto keys = std::vector<QString>();
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

View File

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

View File

@@ -0,0 +1,29 @@
/*
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 <QtCore/QString>
namespace Stripe {
struct PaymentConfiguration {
QString publishableKey;
// PaymentMethodType additionalPaymentMethods; // Apply Pay
// TODO incomplete, not used.
//BillingAddressFields requiredBillingAddressFields
// = BillingAddressFields::None;
QString companyName;
// QString appleMerchantIdentifier; // Apple Pay
// bool smsAutofillDisabled = true; // Mobile only
};
} // namespace Stripe

View File

@@ -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 <QtCore/QString>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonDocument>

View File

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

View File

@@ -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 <QtCore/QDateTime>
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

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
*/
using "ui/basic.style";
using "info/info.style";
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 }};
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);

View File

@@ -0,0 +1,403 @@
/*
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 "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"
#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"
#include <QtCore/QRegularExpression>
namespace Payments::Ui {
namespace {
struct SimpleFieldState {
QString value;
int position = 0;
};
[[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();
}
[[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<ValueValidator>()(QString()))>
[[nodiscard]] auto ComplexNumberValidator(
ValueValidator valueValidator,
Fn<SimpleFieldState(SimpleFieldState)> 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<QString()> 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(
QWidget *parent,
const NativeMethodDetails &native,
CardField field,
not_null<PanelDelegate*> 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 = 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 = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->showError();
}
}
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<RpWidget*> EditCard::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::start_with_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
const auto showBox = [=](object_ptr<BoxContent> box) {
_delegate->panelShowBox(std::move(box));
};
const auto add = [&](FieldConfig &&config) {
auto result = std::make_unique<Field>(inner, std::move(config));
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
return result;
};
_number = add({
.type = FieldType::CardNumber,
.placeholder = tr::lng_payments_card_number(),
.validator = CardNumberValidator(),
});
auto container = inner->add(
object_ptr<FixedHeightWidget>(
inner,
_number->widget()->height()),
st::paymentsFieldPadding);
_expire = std::make_unique<Field>(container, FieldConfig{
.type = FieldType::CardExpireDate,
.placeholder = rpl::single(u"MM / YY"_q),
.validator = ExpireDateValidator(),
});
_cvc = std::make_unique<Field>(container, FieldConfig{
.type = FieldType::CardCVC,
.placeholder = rpl::single(u"CVC"_q),
.validator = CvcValidator([=] { return _number->value(); }),
});
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.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<Ui::FlatLabel>(
inner,
tr::lng_payments_billing_address(),
st::paymentsBillingInformationTitle),
st::paymentsBillingInformationTitlePadding);
}
if (_native.needCountry) {
_country = add({
.type = FieldType::Country,
.placeholder = tr::lng_payments_billing_country(),
.validator = RequiredFinishedValidator(),
.showBox = showBox,
.defaultCountry = _native.defaultCountry,
});
}
if (_native.needZip) {
_zip = add({
.type = FieldType::Text,
.placeholder = tr::lng_payments_billing_zip_code(),
.validator = RequiredValidator(),
});
if (_country) {
_country->finished(
) | rpl::start_with_next([=] {
_zip->setFocus();
}, lifetime());
}
}
return inner;
}
void EditCard::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void EditCard::focusInEvent(QFocusEvent *e) {
if (const auto control = lookupField(_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::lookupField(CardField field) const -> Field* {
switch (field) {
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->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(),
};
}
} // namespace Payments::Ui

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 "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 Field;
class EditCard final : public RpWidget {
public:
EditCard(
QWidget *parent,
const NativeMethodDetails &native,
CardField field,
not_null<PanelDelegate*> delegate);
void setFocus(CardField field);
void setFocusFast(CardField field);
void showError(CardField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(CardField field) const;
[[nodiscard]] UncheckedCardDetails collect() const;
const not_null<PanelDelegate*> _delegate;
NativeMethodDetails _native;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _done;
std::unique_ptr<Field> _number;
std::unique_ptr<Field> _cvc;
std::unique_ptr<Field> _expire;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _zip;
CardField _focusField = CardField::Number;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,229 @@
/*
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 "payments/ui/payments_field.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;
constexpr auto kMinCitySize = 2;
constexpr auto kMaxCitySize = 64;
} // namespace
EditInformation::EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
InformationField field,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _invoice(invoice)
, _information(current)
, _scroll(this, st::passportPanelScroll)
, _topShadow(this)
, _bottomShadow(this)
, _done(
this,
tr::lng_about_done(),
st::passportPanelSaveValue) {
setupControls();
}
EditInformation::~EditInformation() = default;
void EditInformation::setFocus(InformationField field) {
_focusField = field;
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 = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->showError();
}
}
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<RpWidget*> EditInformation::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::start_with_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
const auto showBox = [=](object_ptr<BoxContent> box) {
_delegate->panelShowBox(std::move(box));
};
const auto add = [&](FieldConfig &&config) {
auto result = std::make_unique<Field>(inner, std::move(config));
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
return result;
};
if (_invoice.isShippingAddressRequested) {
_street1 = add({
.placeholder = tr::lng_payments_address_street1(),
.value = _information.shippingAddress.address1,
.validator = RangeLengthValidator(1, kMaxStreetSize),
});
_street2 = add({
.placeholder = tr::lng_payments_address_street2(),
.value = _information.shippingAddress.address2,
.validator = MaxLengthValidator(kMaxStreetSize),
});
_city = add({
.placeholder = tr::lng_payments_address_city(),
.value = _information.shippingAddress.city,
.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),
});
_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,
.validator = RequiredFinishedValidator(),
.showBox = showBox,
.defaultCountry = _information.defaultCountry,
});
_postcode = add({
.placeholder = tr::lng_payments_address_postcode(),
.value = _information.shippingAddress.postcode,
.validator = RangeLengthValidator(1, kMaxPostcodeSize),
});
}
if (_invoice.isNameRequested) {
_name = add({
.placeholder = tr::lng_payments_info_name(),
.value = _information.name,
.validator = RangeLengthValidator(1, kMaxNameSize),
});
}
if (_invoice.isEmailRequested) {
_email = add({
.type = FieldType::Email,
.placeholder = tr::lng_payments_info_email(),
.value = _information.email,
.validator = RangeLengthValidator(1, kMaxEmailSize),
});
}
if (_invoice.isPhoneRequested) {
_phone = add({
.type = FieldType::Phone,
.placeholder = tr::lng_payments_info_phone(),
.value = _information.phone,
.validator = RangeLengthValidator(1, kMaxPhoneSize),
.defaultPhone = _information.defaultPhone,
});
}
return inner;
}
void EditInformation::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void EditInformation::focusInEvent(QFocusEvent *e) {
if (const auto control = lookupField(_focusField)) {
control->setFocus();
}
}
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();
}
auto EditInformation::lookupField(InformationField field) const -> Field* {
switch (field) {
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::lookupField.");
}
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(),
.shippingAddress = {
.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(),
},
};
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,77 @@
/*
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;
class InputField;
class MaskedInputField;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class Field;
class EditInformation final : public RpWidget {
public:
EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
InformationField field,
not_null<PanelDelegate*> delegate);
~EditInformation();
void setFocus(InformationField field);
void setFocusFast(InformationField field);
void showError(InformationField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(InformationField field) const;
[[nodiscard]] RequestedInformation collect() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _done;
std::unique_ptr<Field> _street1;
std::unique_ptr<Field> _street2;
std::unique_ptr<Field> _city;
std::unique_ptr<Field> _state;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _postcode;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _email;
std::unique_ptr<Field> _phone;
InformationField _focusField = InformationField::ShippingStreet;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,361 @@
/*
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 "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 {
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:
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<RpWidget> CreateWrap(
QWidget *parent,
FieldConfig &config) {
switch (config.type) {
case FieldType::Text:
case FieldType::Email:
return base::make_unique_q<InputField>(
parent,
st::paymentsField,
std::move(config.placeholder),
Parse(config));
case FieldType::CardNumber:
case FieldType::CardExpireDate:
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::Phone:
return base::make_unique_q<RpWidget>(parent);
}
Unexpected("FieldType in Payments::Ui::CreateWrap.");
}
[[nodiscard]] InputField *LookupInputField(
not_null<RpWidget*> wrap,
FieldConfig &config) {
return UseMaskedField(config.type)
? nullptr
: static_cast<InputField*>(wrap.get());
}
[[nodiscard]] MaskedInputField *LookupMaskedField(
not_null<RpWidget*> 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:
return CreateChild<MaskedInputField>(
wrap.get(),
st::paymentsField,
std::move(config.placeholder),
Parse(config));
case FieldType::Phone:
return CreateChild<PhoneInput>(
wrap.get(),
st::paymentsField,
std::move(config.placeholder),
ExtractPhonePrefix(config.defaultPhone),
Parse(config));
}
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
}
} // namespace
Field::Field(QWidget *parent, FieldConfig &&config)
: _config(config)
, _wrap(CreateWrap(parent, config))
, _input(LookupInputField(_wrap.get(), config))
, _masked(LookupMaskedField(_wrap.get(), config))
, _countryIso2(config.value) {
if (_masked) {
setupMaskedGeometry();
}
if (_config.type == FieldType::Country) {
setupCountry();
}
if (const auto &validator = config.validator) {
setupValidator(validator);
}
setupFrontBackspace();
}
RpWidget *Field::widget() const {
return _wrap.get();
}
object_ptr<RpWidget> Field::ownedWidget() const {
return object_ptr<RpWidget>::fromRaw(_wrap.get());
}
QString Field::value() const {
return Format(
_config,
_input ? _input->getLastText() : _masked->getLastText(),
_countryIso2);
}
rpl::producer<> Field::frontBackspace() const {
return _frontBackspace.events();
}
rpl::producer<> Field::finished() const {
return _finished.events();
}
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<CountrySelectBox>(
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();
raw->closeBox();
}, _masked->lifetime());
raw->boxClosing() | rpl::start_with_next([=] {
setFocus();
}, _masked->lifetime());
_config.showBox(std::move(box));
});
}
void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> 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<QEvent*> e) {
const auto frontBackspace = (e->type() == QEvent::KeyPress)
&& (static_cast<QKeyEvent*>(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*> field) {
finished() | rpl::start_with_next([=] {
field->setFocus();
}, _masked ? _masked->lifetime() : _input->lifetime());
}
void Field::setPreviousField(not_null<Field*> field) {
frontBackspace(
) | rpl::start_with_next([=] {
field->setFocus();
}, _masked ? _masked->lifetime() : _input->lifetime());
}
void Field::setFocus() {
if (_config.type == FieldType::Country) {
_wrap->setFocus();
} else if (_input) {
_input->setFocus();
} else {
_masked->setFocus();
}
}
void Field::setFocusFast() {
if (_config.type == FieldType::Country) {
setFocus();
} else if (_input) {
_input->setFocusFast();
} else {
_masked->setFocusFast();
}
}
void Field::showError() {
if (_config.type == FieldType::Country) {
setFocus();
_masked->showErrorNoFocus();
} else if (_input) {
_input->showError();
} else {
_masked->showError();
}
}
void Field::showErrorNoFocus() {
if (_input) {
_input->showErrorNoFocus();
} else {
_masked->showErrorNoFocus();
}
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,134 @@
/*
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;
class BoxContent;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
enum class FieldType {
Text,
CardNumber,
CardExpireDate,
CardCVC,
Country,
Phone,
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]] inline 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]] inline auto MaxLengthValidator(int maxLength) {
return RangeLengthValidator(0, maxLength);
}
[[nodiscard]] inline auto RequiredValidator() {
return RangeLengthValidator(1, std::numeric_limits<int>::max());
}
[[nodiscard]] inline 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<QString> placeholder;
QString value;
Fn<FieldValidateResult(FieldValidateRequest)> validator;
Fn<void(object_ptr<BoxContent>)> showBox;
QString defaultPhone;
QString defaultCountry;
};
class Field final {
public:
Field(QWidget *parent, FieldConfig &&config);
[[nodiscard]] RpWidget *widget() const;
[[nodiscard]] object_ptr<RpWidget> 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*> field);
void setPreviousField(not_null<Field*> 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<ValidateResult(ValidateRequest)> validator);
void setupFrontBackspace();
const FieldConfig _config;
const base::unique_qptr<RpWidget> _wrap;
rpl::event_stream<> _frontBackspace;
rpl::event_stream<> _finished;
InputField *_input = nullptr;
MaskedInputField *_masked = nullptr;
QString _countryIso2;
State _was;
bool _validating = false;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,368 @@
/*
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 "settings/settings_common.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 "data/data_countries.h"
#include "lang/lang_keys.h"
#include "base/unixtime.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,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _invoice(invoice)
, _method(method)
, _options(options)
, _information(current)
, _scroll(this, st::passportPanelScroll)
, _topShadow(this)
, _bottomShadow(this)
, _submit(
this,
tr::lng_payments_pay_amount(
lt_amount,
rpl::single(formatAmount(computeTotalAmount()))),
st::paymentsPanelSubmit) {
setupControls();
}
void FormSummary::updateThumbnail(const QImage &thumbnail) {
_invoice.cover.thumbnail = thumbnail;
_thumbnails.fire_copy(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;
}
int64 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 total + shipping;
}
void FormSummary::setupControls() {
const auto inner = setupContent();
_submit->addClickHandler([=] {
_delegate->panelSubmit();
});
if (!_invoice) {
_submit->hide();
}
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));
}
void FormSummary::setupCover(not_null<VerticalLayout*> layout) {
struct State {
QImage thumbnail;
FlatLabel *title = nullptr;
FlatLabel *description = nullptr;
FlatLabel *seller = nullptr;
};
const auto cover = layout->add(object_ptr<RpWidget>(layout));
const auto state = cover->lifetime().make_state<State>();
state->title = CreateChild<FlatLabel>(
cover,
_invoice.cover.title,
st::paymentsTitle);
state->description = CreateChild<FlatLabel>(
cover,
_invoice.cover.description,
st::paymentsDescription);
state->seller = CreateChild<FlatLabel>(
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<VerticalLayout*> layout) {
Settings::AddSkip(layout, st::paymentsPricesTopSkip);
const auto addRow = [&](
const QString &label,
const QString &value,
bool full = false) {
const auto &st = full
? st::paymentsFullPriceAmount
: st::paymentsPriceAmount;
const auto right = CreateChild<FlatLabel>(layout.get(), value, st);
const auto &padding = st::paymentsPricePadding;
const auto left = layout->add(
object_ptr<FlatLabel>(
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());
};
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);
}
const auto selected = ranges::find(
_options.list,
_options.selectedId,
&ShippingOption::id);
if (selected != end(_options.list)) {
for (const auto &price : selected->prices) {
add(price.label, price.price);
}
}
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<VerticalLayout*> layout) {
Settings::AddSkip(layout, st::paymentsSectionsTopSkip);
const auto add = [&](
rpl::producer<QString> title,
const QString &label,
const style::icon *icon,
Fn<void()> handler) {
const auto button = Settings::AddButtonWithLabel(
layout,
std::move(title),
rpl::single(label),
st::paymentsSectionButton,
icon);
button->addClickHandler(std::move(handler));
if (_invoice.receipt) {
button->setAttribute(Qt::WA_TransparentForMouseEvents);
}
};
add(
tr::lng_payments_payment_method(),
_method.title,
&st::paymentsIconPaymentMethod,
[=] { _delegate->panelEditPaymentMethod(); });
if (_invoice.isShippingAddressRequested) {
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(Data::CountryNameByISO2(
_information.shippingAddress.countryIso2));
push(_information.shippingAddress.postcode);
add(
tr::lng_payments_shipping_address(),
list.join(", "),
&st::paymentsIconShippingAddress,
[=] { _delegate->panelEditShippingInformation(); });
}
if (!_options.list.empty()) {
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) {
add(
tr::lng_payments_info_name(),
_information.name,
&st::paymentsIconName,
[=] { _delegate->panelEditName(); });
}
if (_invoice.isEmailRequested) {
add(
tr::lng_payments_info_email(),
_information.email,
&st::paymentsIconEmail,
[=] { _delegate->panelEditEmail(); });
}
if (_invoice.isPhoneRequested) {
add(
tr::lng_payments_info_phone(),
_information.phone,
&st::paymentsIconPhone,
[=] { _delegate->panelEditPhone(); });
}
Settings::AddSkip(layout, st::paymentsSectionsTopSkip);
}
not_null<RpWidget*> FormSummary::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::start_with_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
setupCover(inner);
if (_invoice) {
Settings::AddDivider(inner);
setupPrices(inner);
Settings::AddDivider(inner);
setupSections(inner);
}
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,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 "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class VerticalLayout;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class FormSummary final : public RpWidget {
public:
FormSummary(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate);
void updateThumbnail(const QImage &thumbnail);
private:
void resizeEvent(QResizeEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void setupCover(not_null<VerticalLayout*> layout);
void setupPrices(not_null<VerticalLayout*> layout);
void setupSections(not_null<VerticalLayout*> layout);
void updateControlsGeometry();
[[nodiscard]] QString formatAmount(int64 amount) const;
[[nodiscard]] int64 computeTotalAmount() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
PaymentMethodDetails _method;
ShippingOptions _options;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
rpl::event_stream<QImage> _thumbnails;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,265 @@
/*
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_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"
#include "lang/lang_keys.h"
#include "webview/webview_embed.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->setInnerSize(st::passportPanelSize);
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
_widget->closeRequests(
) | rpl::start_with_next([=] {
_delegate->panelRequestClose();
}, _widget->lifetime());
_widget->closeEvents(
) | rpl::start_with_next([=] {
_delegate->panelCloseSure();
}, _widget->lifetime());
}
Panel::~Panel() {
// Destroy _widget before _webview.
_widget = nullptr;
}
void Panel::requestActivate() {
_widget->showAndActivate();
}
void Panel::showForm(
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options) {
_widget->setTitle(invoice.receipt
? tr::lng_payments_receipt_title()
: tr::lng_payments_checkout_title());
auto form = base::make_unique_q<FormSummary>(
_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 &current,
InformationField field) {
_widget->setTitle(tr::lng_payments_shipping_address_title());
auto edit = base::make_unique_q<EditInformation>(
_widget.get(),
invoice,
current,
field,
_delegate);
_weakEditInformation = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditInformation->setFocusFast(field);
}
void Panel::showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field) {
if (_weakEditInformation) {
_weakEditInformation->showError(field);
} else {
showEditInformation(invoice, current, field);
if (_weakEditInformation
&& field == InformationField::ShippingCountry) {
_weakEditInformation->showError(field);
}
}
}
void Panel::chooseShippingOption(const ShippingOptions &options) {
showBox(Box([=](not_null<Ui::GenericBox*> 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)
? int(i - begin(options.list))
: -1),
.callback = save,
});
}));
}
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)) {
// #TODO payments errors not supported
}
}
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<RpWidget>(_widget.get());
container->setGeometry(_widget->innerGeometry());
container->show();
_webview = std::make_unique<Webview::Window>(
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;
}
showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto save = [=](int option) {
if (option) {
showEditPaymentMethod(method);
}
};
SingleChoiceBox(box, {
.title = tr::lng_payments_payment_method(),
.options = { method.title, tr::lng_payments_new_card(tr::now) },
.initialSelection = 0,
.callback = save,
});
}));
}
void Panel::showEditCard(
const NativeMethodDetails &native,
CardField field) {
Expects(native.supported);
auto edit = base::make_unique_q<EditCard>(
_widget.get(),
native,
field,
_delegate);
_weakEditCard = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditCard->setFocusFast(field);
}
void Panel::showCardError(
const NativeMethodDetails &native,
CardField field) {
if (_weakEditCard) {
_weakEditCard->showError(field);
} else {
// We cancelled card edit already.
//showEditCard(native, field);
//if (_weakEditCard
// && field == CardField::AddressCountry) {
// _weakEditCard->showError(field);
//}
}
}
rpl::producer<> Panel::backRequests() const {
return _widget->backRequests();
}
void Panel::showBox(object_ptr<Ui::BoxContent> box) {
_widget->showBox(
std::move(box),
Ui::LayerOption::KeepOther,
anim::type::normal);
}
void Panel::showToast(const TextWithEntities &text) {
_widget->showToast(text);
}
rpl::lifetime &Panel::lifetime() {
return _widget->lifetime();
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,89 @@
/*
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"
namespace Ui {
class SeparatePanel;
class BoxContent;
} // namespace Ui
namespace Webview {
class Window;
} // namespace Webview
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
struct Invoice;
struct RequestedInformation;
struct ShippingOptions;
enum class InformationField;
enum class CardField;
class FormSummary;
class EditInformation;
class EditCard;
struct PaymentMethodDetails;
struct NativeMethodDetails;
class Panel final {
public:
explicit Panel(not_null<PanelDelegate*> delegate);
~Panel();
void requestActivate();
void showForm(
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options);
void updateFormThumbnail(const QImage &thumbnail);
void showEditInformation(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field);
void showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field);
void showEditPaymentMethod(const PaymentMethodDetails &method);
void showEditCard(
const NativeMethodDetails &native,
CardField field);
void showCardError(
const NativeMethodDetails &native,
CardField field);
void chooseShippingOption(const ShippingOptions &options);
void choosePaymentMethod(const PaymentMethodDetails &method);
bool showWebview(const QString &url, bool allowBack);
[[nodiscard]] rpl::producer<> backRequests() const;
void showBox(object_ptr<Ui::BoxContent> box);
void showToast(const TextWithEntities &text);
[[nodiscard]] rpl::lifetime &lifetime();
private:
bool createWebview();
const not_null<PanelDelegate*> _delegate;
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<Webview::Window> _webview;
QPointer<FormSummary> _weakFormSummary;
QPointer<EditInformation> _weakEditInformation;
QPointer<EditCard> _weakEditCard;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,180 @@
/*
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;
int64 price = 0;
};
struct Cover {
QString title;
QString description;
QString seller;
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<LabeledPrice> prices;
QString currency;
Receipt receipt;
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 ShippingOption {
QString id;
QString title;
std::vector<LabeledPrice> prices;
};
struct ShippingOptions {
std::vector<ShippingOption> list;
QString selectedId;
};
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();
}
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 RequestedInformation {
QString defaultPhone;
QString defaultCountry;
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();
}
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);
}
};
enum class InformationField {
ShippingStreet,
ShippingCity,
ShippingState,
ShippingCountry,
ShippingPostcode,
Name,
Email,
Phone,
};
struct NativeMethodDetails {
QString defaultCountry;
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,
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

View File

@@ -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 "base/object_ptr.h"
class QJsonDocument;
class QString;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
struct RequestedInformation;
struct UncheckedCardDetails;
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;
virtual void panelEditPaymentMethod() = 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 panelValidateCard(Ui::UncheckedCardDetails data) = 0;
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
virtual QString panelWebviewDataPath() = 0;
};
} // namespace Payments::Ui

View File

@@ -65,8 +65,8 @@ bool GdkHelperLoadGtk2(QLibrary &lib) {
#ifdef LINK_TO_GTK
return false;
#else // LINK_TO_GTK
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_drawable_get_xdisplay", gdk_x11_drawable_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_drawable_get_xid", gdk_x11_drawable_get_xid)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xid)) return false;
return true;
#endif // !LINK_TO_GTK
#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION
@@ -76,10 +76,10 @@ bool GdkHelperLoadGtk2(QLibrary &lib) {
bool GdkHelperLoadGtk3(QLibrary &lib) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_window_get_type", gdk_x11_window_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_window_get_display", gdk_window_get_display)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_display_get_xdisplay", gdk_x11_display_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_window_get_xid", gdk_x11_window_get_xid)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_window_get_display)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_x11_display_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_xid)) return false;
return true;
#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;

View File

@@ -60,73 +60,73 @@ void GtkIntegration::load() {
auto &lib = BaseGtkIntegration::Instance()->library();
LOAD_GTK_SYMBOL(lib, "gtk_widget_show", gtk_widget_show);
LOAD_GTK_SYMBOL(lib, "gtk_widget_hide", gtk_widget_hide);
LOAD_GTK_SYMBOL(lib, "gtk_widget_get_window", gtk_widget_get_window);
LOAD_GTK_SYMBOL(lib, "gtk_widget_realize", gtk_widget_realize);
LOAD_GTK_SYMBOL(lib, "gtk_widget_hide_on_delete", gtk_widget_hide_on_delete);
LOAD_GTK_SYMBOL(lib, "gtk_widget_destroy", gtk_widget_destroy);
LOAD_GTK_SYMBOL(lib, "gtk_clipboard_get", gtk_clipboard_get);
LOAD_GTK_SYMBOL(lib, "gtk_clipboard_store", gtk_clipboard_store);
LOAD_GTK_SYMBOL(lib, "gtk_clipboard_wait_for_contents", gtk_clipboard_wait_for_contents);
LOAD_GTK_SYMBOL(lib, "gtk_clipboard_wait_for_image", gtk_clipboard_wait_for_image);
LOAD_GTK_SYMBOL(lib, "gtk_selection_data_targets_include_image", gtk_selection_data_targets_include_image);
LOAD_GTK_SYMBOL(lib, "gtk_selection_data_free", gtk_selection_data_free);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_dialog_new", gtk_file_chooser_dialog_new);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_get_type", gtk_file_chooser_get_type);
LOAD_GTK_SYMBOL(lib, "gtk_image_get_type", gtk_image_get_type);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_current_folder", gtk_file_chooser_set_current_folder);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_get_current_folder", gtk_file_chooser_get_current_folder);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_current_name", gtk_file_chooser_set_current_name);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_select_filename", gtk_file_chooser_select_filename);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_get_filenames", gtk_file_chooser_get_filenames);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_filter", gtk_file_chooser_set_filter);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_get_filter", gtk_file_chooser_get_filter);
LOAD_GTK_SYMBOL(lib, "gtk_window_get_type", gtk_window_get_type);
LOAD_GTK_SYMBOL(lib, "gtk_window_set_title", gtk_window_set_title);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_local_only", gtk_file_chooser_set_local_only);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_action", gtk_file_chooser_set_action);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_select_multiple", gtk_file_chooser_set_select_multiple);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_do_overwrite_confirmation", gtk_file_chooser_set_do_overwrite_confirmation);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_remove_filter", gtk_file_chooser_remove_filter);
LOAD_GTK_SYMBOL(lib, "gtk_file_filter_set_name", gtk_file_filter_set_name);
LOAD_GTK_SYMBOL(lib, "gtk_file_filter_add_pattern", gtk_file_filter_add_pattern);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_add_filter", gtk_file_chooser_add_filter);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_preview_widget", gtk_file_chooser_set_preview_widget);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_get_preview_filename", gtk_file_chooser_get_preview_filename);
LOAD_GTK_SYMBOL(lib, "gtk_file_chooser_set_preview_widget_active", gtk_file_chooser_set_preview_widget_active);
LOAD_GTK_SYMBOL(lib, "gtk_file_filter_new", gtk_file_filter_new);
LOAD_GTK_SYMBOL(lib, "gtk_image_new", gtk_image_new);
LOAD_GTK_SYMBOL(lib, "gtk_image_set_from_pixbuf", gtk_image_set_from_pixbuf);
LOAD_GTK_SYMBOL(lib, gtk_widget_show);
LOAD_GTK_SYMBOL(lib, gtk_widget_hide);
LOAD_GTK_SYMBOL(lib, gtk_widget_get_window);
LOAD_GTK_SYMBOL(lib, gtk_widget_realize);
LOAD_GTK_SYMBOL(lib, gtk_widget_hide_on_delete);
LOAD_GTK_SYMBOL(lib, gtk_widget_destroy);
LOAD_GTK_SYMBOL(lib, gtk_clipboard_get);
LOAD_GTK_SYMBOL(lib, gtk_clipboard_store);
LOAD_GTK_SYMBOL(lib, gtk_clipboard_wait_for_contents);
LOAD_GTK_SYMBOL(lib, gtk_clipboard_wait_for_image);
LOAD_GTK_SYMBOL(lib, gtk_selection_data_targets_include_image);
LOAD_GTK_SYMBOL(lib, gtk_selection_data_free);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_dialog_new);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_get_type);
LOAD_GTK_SYMBOL(lib, gtk_image_get_type);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_current_folder);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_get_current_folder);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_current_name);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_select_filename);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_get_filenames);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_filter);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_get_filter);
LOAD_GTK_SYMBOL(lib, gtk_window_get_type);
LOAD_GTK_SYMBOL(lib, gtk_window_set_title);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_local_only);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_action);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_select_multiple);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_do_overwrite_confirmation);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_remove_filter);
LOAD_GTK_SYMBOL(lib, gtk_file_filter_set_name);
LOAD_GTK_SYMBOL(lib, gtk_file_filter_add_pattern);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_add_filter);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_preview_widget);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_get_preview_filename);
LOAD_GTK_SYMBOL(lib, gtk_file_chooser_set_preview_widget_active);
LOAD_GTK_SYMBOL(lib, gtk_file_filter_new);
LOAD_GTK_SYMBOL(lib, gtk_image_new);
LOAD_GTK_SYMBOL(lib, gtk_image_set_from_pixbuf);
LOAD_GTK_SYMBOL(lib, "gdk_window_set_modal_hint", gdk_window_set_modal_hint);
LOAD_GTK_SYMBOL(lib, "gdk_window_focus", gdk_window_focus);
LOAD_GTK_SYMBOL(lib, "gtk_dialog_get_type", gtk_dialog_get_type);
LOAD_GTK_SYMBOL(lib, "gtk_dialog_run", gtk_dialog_run);
LOAD_GTK_SYMBOL(lib, gdk_window_set_modal_hint);
LOAD_GTK_SYMBOL(lib, gdk_window_focus);
LOAD_GTK_SYMBOL(lib, gtk_dialog_get_type);
LOAD_GTK_SYMBOL(lib, gtk_dialog_run);
LOAD_GTK_SYMBOL(lib, "gdk_atom_intern", gdk_atom_intern);
LOAD_GTK_SYMBOL(lib, gdk_atom_intern);
LOAD_GTK_SYMBOL(lib, "gdk_display_get_default", gdk_display_get_default);
LOAD_GTK_SYMBOL(lib, "gdk_display_get_monitor", gdk_display_get_monitor);
LOAD_GTK_SYMBOL(lib, "gdk_display_get_primary_monitor", gdk_display_get_primary_monitor);
LOAD_GTK_SYMBOL(lib, "gdk_monitor_get_scale_factor", gdk_monitor_get_scale_factor);
LOAD_GTK_SYMBOL(lib, gdk_display_get_default);
LOAD_GTK_SYMBOL(lib, gdk_display_get_monitor);
LOAD_GTK_SYMBOL(lib, gdk_display_get_primary_monitor);
LOAD_GTK_SYMBOL(lib, gdk_monitor_get_scale_factor);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_new_from_file_at_size", gdk_pixbuf_new_from_file_at_size);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_get_has_alpha", gdk_pixbuf_get_has_alpha);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_get_pixels", gdk_pixbuf_get_pixels);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_get_width", gdk_pixbuf_get_width);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_get_height", gdk_pixbuf_get_height);
LOAD_GTK_SYMBOL(lib, "gdk_pixbuf_get_rowstride", gdk_pixbuf_get_rowstride);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_new_from_file_at_size);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_get_has_alpha);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_get_pixels);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_get_width);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_get_height);
LOAD_GTK_SYMBOL(lib, gdk_pixbuf_get_rowstride);
GdkHelperLoad(lib);
LOAD_GTK_SYMBOL(lib, "gtk_dialog_get_widget_for_response", gtk_dialog_get_widget_for_response);
LOAD_GTK_SYMBOL(lib, "gtk_button_set_label", gtk_button_set_label);
LOAD_GTK_SYMBOL(lib, "gtk_button_get_type", gtk_button_get_type);
LOAD_GTK_SYMBOL(lib, gtk_dialog_get_widget_for_response);
LOAD_GTK_SYMBOL(lib, gtk_button_set_label);
LOAD_GTK_SYMBOL(lib, gtk_button_get_type);
LOAD_GTK_SYMBOL(lib, "gtk_app_chooser_dialog_new", gtk_app_chooser_dialog_new);
LOAD_GTK_SYMBOL(lib, "gtk_app_chooser_get_app_info", gtk_app_chooser_get_app_info);
LOAD_GTK_SYMBOL(lib, "gtk_app_chooser_get_type", gtk_app_chooser_get_type);
LOAD_GTK_SYMBOL(lib, gtk_app_chooser_dialog_new);
LOAD_GTK_SYMBOL(lib, gtk_app_chooser_get_app_info);
LOAD_GTK_SYMBOL(lib, gtk_app_chooser_get_type);
Loaded = true;
}

View File

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

View File

@@ -273,4 +273,8 @@ void Domain::clearOldVersion() {
_oldVersion = 0;
}
QString Domain::webviewDataPath() const {
return BaseGlobalPath() + "webview";
}
} // namespace Storage

View File

@@ -44,6 +44,8 @@ public:
[[nodiscard]] int oldVersion() const;
void clearOldVersion();
[[nodiscard]] QString webviewDataPath() const;
private:
enum class StartModernResult {
Success,

View File

@@ -0,0 +1,452 @@
/*
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 <QtCore/QRegularExpression>
namespace Ui {
namespace {
QString LastValidISO;
} // namespace
class CountrySelectBox::Inner : public TWidget {
public:
Inner(QWidget *parent, const QString &iso, 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<QString> countryChosen() const {
return _countryChosen.events();
}
[[nodiscard]] rpl::producer<ScrollToRequest> 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<not_null<const Data::CountryInfo*>> &current() const;
Type _type = Type::Phones;
int _rowHeight = 0;
int _selected = -1;
int _pressed = -1;
QString _filter;
bool _mouseSelection = false;
std::vector<std::unique_ptr<RippleAnimation>> _ripples;
std::vector<not_null<const Data::CountryInfo*>> _list;
std::vector<not_null<const Data::CountryInfo*>> _filtered;
base::flat_map<QChar, std::vector<int>> _byLetter;
std::vector<std::vector<QString>> _namesList;
rpl::event_stream<QString> _countryChosen;
rpl::event_stream<ScrollToRequest> _mustScrollTo;
};
CountrySelectBox::CountrySelectBox(QWidget*)
: CountrySelectBox(nullptr, QString(), Type::Phones) {
}
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
: _type(type)
, _select(this, st::defaultMultiSelect, tr::lng_country_ph())
, _ownedInner(this, iso, type) {
}
rpl::producer<QString> CountrySelectBox::countryChosen() const {
Expects(_ownedInner != nullptr || _inner != nullptr);
return (_ownedInner
? _ownedInner.data()
: _inner.data())->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(
std::move(_ownedInner),
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,
const QString &iso,
Type type)
: TWidget(parent)
, _type(type)
, _rowHeight(st::countryRowHeight) {
setAttribute(Qt::WA_OpaquePaintEvent);
const auto &byISO2 = Data::CountriesByISO2();
if (byISO2.contains(iso)) {
LastValidISO = iso;
}
_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<RippleAnimation>(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<QString> &names,
const QString &word) {
for (const auto &name : names) {
if (name.startsWith(word)) {
return true;
}
}
return false;
};
const auto hasAllWords = [&](const std::vector<QString> &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<not_null<const Data::CountryInfo*>> & {
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

View File

@@ -0,0 +1,54 @@
/*
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<QString> 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<MultiSelect> _select;
class Inner;
object_ptr<Inner> _ownedInner;
QPointer<Inner> _inner;
};
} // namespace Ui

View File

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

View File

@@ -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<CountrySelectBox>());
connect(box, SIGNAL(countryChosen(const QString&)), this, SLOT(onChooseCountry(const QString&)));
auto box = Ui::show(Box<Ui::CountrySelectBox>());
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<Inner>(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<Ui::RippleAnimation>(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<QString> &names,
const QString &word) {
for (const auto &name : names) {
if (name.startsWith(word)) {
return true;
}
}
return false;
};
const auto hasAllWords = [&](const std::vector<QString> &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<not_null<const Data::CountryInfo*>> & {
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;

View File

@@ -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<Ui::MultiSelect> _select;
class Inner;
QPointer<Inner> _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<not_null<const Data::CountryInfo*>> &current() const;
Type _type = Type::Phones;
int _rowHeight = 0;
int _selected = -1;
int _pressed = -1;
QString _filter;
bool _mouseSelection = false;
std::vector<std::unique_ptr<Ui::RippleAnimation>> _ripples;
std::vector<not_null<const Data::CountryInfo*>> _list;
std::vector<not_null<const Data::CountryInfo*>> _filtered;
base::flat_map<QChar, std::vector<int>> _byLetter;
std::vector<std::vector<QString>> _namesList;
};

View File

@@ -7,23 +7,30 @@ 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 <QtCore/QRegularExpression>
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(
QWidget *parent,
const style::InputField &st)
: MaskedInputField(parent, st)
, _nosignal(false) {
: MaskedInputField(parent, st) {
}
void CountryCodeInput::startErasing(QKeyEvent *e) {
@@ -83,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) {
@@ -111,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);
}
@@ -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;
@@ -194,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();
@@ -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;

View File

@@ -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<QString> addedToNumber() const {
return _addedToNumber.events();
}
[[nodiscard]] rpl::producer<QString> 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<QString> _addedToNumber;
rpl::event_stream<QString> _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<not_null<QKeyEvent*>> {
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<int> _pattern;
QString _additionalPlaceholder;
rpl::event_stream<not_null<QKeyEvent*>> _frontBackspaceEvent;
};
@@ -90,6 +93,8 @@ private:
};
[[nodiscard]] QString ExtractPhonePrefix(const QString &phone);
class PhoneInput : public MaskedInputField {
public:
PhoneInput(

Some files were not shown because too many files have changed in this diff Show More