Compare commits
27 Commits
ci
...
linux_webv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
672aacd528 | ||
|
|
1a5e807fa9 | ||
|
|
f98f4f0d14 | ||
|
|
78def16ced | ||
|
|
cd7b3419de | ||
|
|
dc4048f1c1 | ||
|
|
bef5320163 | ||
|
|
9a722ea8d4 | ||
|
|
1aefada45d | ||
|
|
f5a8bf0e74 | ||
|
|
738439c334 | ||
|
|
8949c9969b | ||
|
|
cdf6fb1512 | ||
|
|
fb0ea59ff3 | ||
|
|
36f5be60f4 | ||
|
|
46508f7e5e | ||
|
|
28137dfb60 | ||
|
|
e7784620d3 | ||
|
|
462986e9c3 | ||
|
|
c11de2380e | ||
|
|
a432e826a6 | ||
|
|
ea9e85e70f | ||
|
|
d3829c52ec | ||
|
|
9f04570335 | ||
|
|
3c486522a7 | ||
|
|
07cd8c4e83 | ||
|
|
7447c6ea75 |
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
Telegram/Resources/icons/payments/payment_address.png
Normal file
|
After Width: | Height: | Size: 802 B |
BIN
Telegram/Resources/icons/payments/payment_address@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/payments/payment_address@3x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Telegram/Resources/icons/payments/payment_card.png
Normal file
|
After Width: | Height: | Size: 357 B |
BIN
Telegram/Resources/icons/payments/payment_card@2x.png
Normal file
|
After Width: | Height: | Size: 560 B |
BIN
Telegram/Resources/icons/payments/payment_card@3x.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
Telegram/Resources/icons/payments/payment_email.png
Normal file
|
After Width: | Height: | Size: 949 B |
BIN
Telegram/Resources/icons/payments/payment_email@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Telegram/Resources/icons/payments/payment_email@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Telegram/Resources/icons/payments/payment_name.png
Normal file
|
After Width: | Height: | Size: 473 B |
BIN
Telegram/Resources/icons/payments/payment_name@2x.png
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
Telegram/Resources/icons/payments/payment_name@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/payments/payment_phone.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
Telegram/Resources/icons/payments/payment_phone@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/payments/payment_phone@3x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Telegram/Resources/icons/payments/payment_shipping.png
Normal file
|
After Width: | Height: | Size: 526 B |
BIN
Telegram/Resources/icons/payments/payment_shipping@2x.png
Normal file
|
After Width: | Height: | Size: 1022 B |
BIN
Telegram/Resources/icons/payments/payment_shipping@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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...";
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1820,7 +1820,7 @@ Utf8String FormatDateTime(
|
||||
).toUtf8();
|
||||
}
|
||||
|
||||
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy) {
|
||||
Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy) {
|
||||
return Ui::FillAmountAndCurrency(
|
||||
amount,
|
||||
QString::fromUtf8(currency)).toUtf8();
|
||||
|
||||
@@ -660,7 +660,7 @@ Utf8String FormatDateTime(
|
||||
QChar dateSeparator = QChar('.'),
|
||||
QChar timeSeparator = QChar(':'),
|
||||
QChar separator = QChar(' '));
|
||||
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy);
|
||||
Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy);
|
||||
Utf8String FormatFileSize(int64 size);
|
||||
Utf8String FormatDuration(int64 seconds);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ void Panel::showBox(
|
||||
}
|
||||
|
||||
void Panel::showToast(const QString &text) {
|
||||
_widget->showToast(text);
|
||||
_widget->showToast({ text });
|
||||
}
|
||||
|
||||
Panel::~Panel() = default;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
125
Telegram/SourceFiles/passport/ui/passport_form_row.cpp
Normal 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
|
||||
48
Telegram/SourceFiles/passport/ui/passport_form_row.h
Normal 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
|
||||
462
Telegram/SourceFiles/payments/payments_checkout_process.cpp
Normal 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
|
||||
98
Telegram/SourceFiles/payments/payments_checkout_process.h
Normal 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
|
||||
669
Telegram/SourceFiles/payments/payments_form.cpp
Normal 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
|
||||
243
Telegram/SourceFiles/payments/payments_form.h
Normal 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
|
||||
18
Telegram/SourceFiles/payments/stripe/stripe_address.h
Normal 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
|
||||
167
Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp
Normal 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
|
||||
44
Telegram/SourceFiles/payments/stripe/stripe_api_client.h
Normal 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
|
||||
19
Telegram/SourceFiles/payments/stripe/stripe_callbacks.h
Normal 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
|
||||
188
Telegram/SourceFiles/payments/stripe/stripe_card.cpp
Normal 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
|
||||
99
Telegram/SourceFiles/payments/stripe/stripe_card.h
Normal 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
|
||||
33
Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp
Normal 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
|
||||
32
Telegram/SourceFiles/payments/stripe/stripe_card_params.h
Normal 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
|
||||
279
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal 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
|
||||
51
Telegram/SourceFiles/payments/stripe/stripe_card_validator.h
Normal 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
|
||||
23
Telegram/SourceFiles/payments/stripe/stripe_decode.cpp
Normal 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
|
||||
18
Telegram/SourceFiles/payments/stripe/stripe_decode.h
Normal 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
|
||||
107
Telegram/SourceFiles/payments/stripe/stripe_error.cpp
Normal 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
|
||||
66
Telegram/SourceFiles/payments/stripe/stripe_error.h
Normal 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 ¶meter = 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
|
||||
39
Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h
Normal 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
|
||||
40
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp
Normal 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
|
||||
21
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h
Normal 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
|
||||
@@ -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
|
||||
13
Telegram/SourceFiles/payments/stripe/stripe_pch.h
Normal 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>
|
||||
65
Telegram/SourceFiles/payments/stripe/stripe_token.cpp
Normal 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
|
||||
49
Telegram/SourceFiles/payments/stripe/stripe_token.h
Normal 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
|
||||
69
Telegram/SourceFiles/payments/ui/payments.style
Normal 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);
|
||||
403
Telegram/SourceFiles/payments/ui/payments_edit_card.cpp
Normal 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
|
||||
69
Telegram/SourceFiles/payments/ui/payments_edit_card.h
Normal 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
|
||||
229
Telegram/SourceFiles/payments/ui/payments_edit_information.cpp
Normal 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 ¤t,
|
||||
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
|
||||
77
Telegram/SourceFiles/payments/ui/payments_edit_information.h
Normal 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 ¤t,
|
||||
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
|
||||
361
Telegram/SourceFiles/payments/ui/payments_field.cpp
Normal 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
|
||||
134
Telegram/SourceFiles/payments/ui/payments_field.h
Normal 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
|
||||
368
Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
Normal 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 ¤t,
|
||||
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
|
||||
65
Telegram/SourceFiles/payments/ui/payments_form_summary.h
Normal 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 ¤t,
|
||||
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
|
||||
265
Telegram/SourceFiles/payments/ui/payments_panel.cpp
Normal 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 ¤t,
|
||||
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 ¤t,
|
||||
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 ¤t,
|
||||
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
|
||||
89
Telegram/SourceFiles/payments/ui/payments_panel.h
Normal 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 ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options);
|
||||
void updateFormThumbnail(const QImage &thumbnail);
|
||||
void showEditInformation(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field);
|
||||
void showInformationError(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
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
|
||||
180
Telegram/SourceFiles/payments/ui/payments_panel_data.h
Normal 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
|
||||
49
Telegram/SourceFiles/payments/ui/payments_panel_delegate.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -273,4 +273,8 @@ void Domain::clearOldVersion() {
|
||||
_oldVersion = 0;
|
||||
}
|
||||
|
||||
QString Domain::webviewDataPath() const {
|
||||
return BaseGlobalPath() + "webview";
|
||||
}
|
||||
|
||||
} // namespace Storage
|
||||
|
||||
@@ -44,6 +44,8 @@ public:
|
||||
[[nodiscard]] int oldVersion() const;
|
||||
void clearOldVersion();
|
||||
|
||||
[[nodiscard]] QString webviewDataPath() const;
|
||||
|
||||
private:
|
||||
enum class StartModernResult {
|
||||
Success,
|
||||
|
||||
452
Telegram/SourceFiles/ui/boxes/country_select_box.cpp
Normal 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*>> ¤t() 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
|
||||
54
Telegram/SourceFiles/ui/boxes/country_select_box.h
Normal 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
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -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*>> ¤t() 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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||