mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-06 15:13:57 +02:00
Add local validation for card information.
This commit is contained in:
parent
e077163322
commit
0af6c4b0b6
23 changed files with 950 additions and 98 deletions
|
@ -1029,8 +1029,6 @@ PRIVATE
|
||||||
ui/search_field_controller.h
|
ui/search_field_controller.h
|
||||||
ui/special_buttons.cpp
|
ui/special_buttons.cpp
|
||||||
ui/special_buttons.h
|
ui/special_buttons.h
|
||||||
ui/special_fields.cpp
|
|
||||||
ui/special_fields.h
|
|
||||||
ui/unread_badge.cpp
|
ui/unread_badge.cpp
|
||||||
ui/unread_badge.h
|
ui/unread_badge.h
|
||||||
window/main_window.cpp
|
window/main_window.cpp
|
||||||
|
|
|
@ -151,7 +151,7 @@ void ChangePhoneBox::EnterPhone::prepare() {
|
||||||
this,
|
this,
|
||||||
st::defaultInputField,
|
st::defaultInputField,
|
||||||
tr::lng_change_phone_new_title(),
|
tr::lng_change_phone_new_title(),
|
||||||
ExtractPhonePrefix(_session->user()->phone()),
|
Ui::ExtractPhonePrefix(_session->user()->phone()),
|
||||||
phoneValue);
|
phoneValue);
|
||||||
|
|
||||||
_phone->resize(st::boxWidth - 2 * st::boxPadding.left(), _phone->height());
|
_phone->resize(st::boxWidth - 2 * st::boxPadding.left(), _phone->height());
|
||||||
|
|
|
@ -71,14 +71,6 @@ void ShowPhoneBannedError(const QString &phone) {
|
||||||
[=] { SendToBannedHelp(phone); close(); }));
|
[=] { 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(
|
SentCodeField::SentCodeField(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
const style::InputField &st,
|
const style::InputField &st,
|
||||||
|
|
|
@ -22,7 +22,6 @@ class Session;
|
||||||
} // namespace Main
|
} // namespace Main
|
||||||
|
|
||||||
void ShowPhoneBannedError(const QString &phone);
|
void ShowPhoneBannedError(const QString &phone);
|
||||||
[[nodiscard]] QString ExtractPhonePrefix(const QString &phone);
|
|
||||||
|
|
||||||
class SentCodeField : public Ui::InputField {
|
class SentCodeField : public Ui::InputField {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -13,9 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
enum {
|
enum {
|
||||||
MaxSelectedItems = 100,
|
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
|
LocalEncryptIterCount = 4000, // key derivation iteration count
|
||||||
LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)
|
LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)
|
||||||
LocalEncryptSaltSize = 32, // 256 bit
|
LocalEncryptSaltSize = 32, // 256 bit
|
||||||
|
|
|
@ -278,7 +278,8 @@ void PanelEditContact::setupControls(
|
||||||
wrap.data(),
|
wrap.data(),
|
||||||
fieldStyle,
|
fieldStyle,
|
||||||
std::move(fieldPlaceholder),
|
std::move(fieldPlaceholder),
|
||||||
ExtractPhonePrefix(_controller->bot()->session().user()->phone()),
|
Ui::ExtractPhonePrefix(
|
||||||
|
_controller->bot()->session().user()->phone()),
|
||||||
data);
|
data);
|
||||||
} else {
|
} else {
|
||||||
_field = Ui::CreateChild<Ui::MaskedInputField>(
|
_field = Ui::CreateChild<Ui::MaskedInputField>(
|
||||||
|
|
|
@ -166,23 +166,36 @@ void CheckoutProcess::handleError(const Error &error) {
|
||||||
showForm();
|
showForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
using Field = Ui::InformationField;
|
using InfoField = Ui::InformationField;
|
||||||
|
using CardField = Ui::CardField;
|
||||||
if (id == u"REQ_INFO_NAME_INVALID"_q) {
|
if (id == u"REQ_INFO_NAME_INVALID"_q) {
|
||||||
showInformationError(Field::Name);
|
showInformationError(InfoField::Name);
|
||||||
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
|
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
|
||||||
showInformationError(Field::Email);
|
showInformationError(InfoField::Email);
|
||||||
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
|
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
|
||||||
showInformationError(Field::Phone);
|
showInformationError(InfoField::Phone);
|
||||||
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
|
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
|
||||||
showInformationError(Field::ShippingStreet);
|
showInformationError(InfoField::ShippingStreet);
|
||||||
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
|
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
|
||||||
showInformationError(Field::ShippingCity);
|
showInformationError(InfoField::ShippingCity);
|
||||||
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
|
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
|
||||||
showInformationError(Field::ShippingState);
|
showInformationError(InfoField::ShippingState);
|
||||||
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
|
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
|
||||||
showInformationError(Field::ShippingCountry);
|
showInformationError(InfoField::ShippingCountry);
|
||||||
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
|
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
|
||||||
showInformationError(Field::ShippingPostcode);
|
showInformationError(InfoField::ShippingPostcode);
|
||||||
|
} else if (id == u"LOCAL_CARD_NUMBER_INVALID"_q) {
|
||||||
|
showCardError(CardField::Number);
|
||||||
|
} else if (id == u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q) {
|
||||||
|
showCardError(CardField::ExpireDate);
|
||||||
|
} else if (id == u"LOCAL_CARD_CVC_INVALID"_q) {
|
||||||
|
showCardError(CardField::Cvc);
|
||||||
|
} else if (id == u"LOCAL_CARD_HOLDER_NAME_INVALID"_q) {
|
||||||
|
showCardError(CardField::Name);
|
||||||
|
} else if (id == u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q) {
|
||||||
|
showCardError(CardField::AddressCountry);
|
||||||
|
} else if (id == u"LOCAL_CARD_BILLING_ZIP_INVALID"_q) {
|
||||||
|
showCardError(CardField::AddressZip);
|
||||||
} else if (id == u"SHIPPING_BOT_TIMEOUT"_q) {
|
} else if (id == u"SHIPPING_BOT_TIMEOUT"_q) {
|
||||||
showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message
|
showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message
|
||||||
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
|
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
|
||||||
|
@ -196,11 +209,17 @@ void CheckoutProcess::handleError(const Error &error) {
|
||||||
if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) {
|
if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) {
|
||||||
showCardError(Field::Number);
|
showCardError(Field::Number);
|
||||||
} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
|
} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
|
||||||
showCardError(Field::CVC);
|
showCardError(Field::Cvc);
|
||||||
} else if (id == u"InvalidExpiryMonth"_q
|
} else if (id == u"InvalidExpiryMonth"_q
|
||||||
|| id == u"InvalidExpiryYear"_q
|
|| id == u"InvalidExpiryYear"_q
|
||||||
|| id == u"ExpiredCard"_q) {
|
|| id == u"ExpiredCard"_q) {
|
||||||
showCardError(Field::ExpireDate);
|
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 {
|
} else {
|
||||||
showToast({ "Error: " + id });
|
showToast({ "Error: " + id });
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "stripe/stripe_api_client.h"
|
#include "stripe/stripe_api_client.h"
|
||||||
#include "stripe/stripe_error.h"
|
#include "stripe/stripe_error.h"
|
||||||
#include "stripe/stripe_token.h"
|
#include "stripe/stripe_token.h"
|
||||||
|
#include "stripe/stripe_card_validator.h"
|
||||||
#include "ui/image/image.h"
|
#include "ui/image/image.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "styles/style_payments.h" // paymentsThumbnailSize.
|
#include "styles/style_payments.h" // paymentsThumbnailSize.
|
||||||
|
@ -270,6 +271,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) {
|
||||||
void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
|
void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
|
||||||
const auto address = data.vshipping_address();
|
const auto address = data.vshipping_address();
|
||||||
_savedInformation = Ui::RequestedInformation{
|
_savedInformation = Ui::RequestedInformation{
|
||||||
|
.defaultPhone = defaultPhone(),
|
||||||
.defaultCountry = defaultCountry(),
|
.defaultCountry = defaultCountry(),
|
||||||
.name = qs(data.vname().value_or_empty()),
|
.name = qs(data.vname().value_or_empty()),
|
||||||
.phone = qs(data.vphone().value_or_empty()),
|
.phone = qs(data.vphone().value_or_empty()),
|
||||||
|
@ -293,11 +295,16 @@ void Form::refreshPaymentMethodDetails() {
|
||||||
const auto &entered = _paymentMethod.newCredentials;
|
const auto &entered = _paymentMethod.newCredentials;
|
||||||
_paymentMethod.ui.title = entered ? entered.title : saved.title;
|
_paymentMethod.ui.title = entered ? entered.title : saved.title;
|
||||||
_paymentMethod.ui.ready = entered || saved;
|
_paymentMethod.ui.ready = entered || saved;
|
||||||
|
_paymentMethod.ui.native.defaultPhone = defaultPhone();
|
||||||
_paymentMethod.ui.native.defaultCountry = defaultCountry();
|
_paymentMethod.ui.native.defaultCountry = defaultCountry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Form::defaultPhone() const {
|
||||||
|
return _session->user()->phone();
|
||||||
|
}
|
||||||
|
|
||||||
QString Form::defaultCountry() const {
|
QString Form::defaultCountry() const {
|
||||||
return Data::CountryISO2ByPhone(_session->user()->phone());
|
return Data::CountryISO2ByPhone(defaultPhone());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Form::fillPaymentMethodInformation() {
|
void Form::fillPaymentMethodInformation() {
|
||||||
|
@ -382,10 +389,16 @@ void Form::validateInformation(const Ui::RequestedInformation &information) {
|
||||||
_api.request(base::take(_validateRequestId)).cancel();
|
_api.request(base::take(_validateRequestId)).cancel();
|
||||||
}
|
}
|
||||||
_validatedInformation = information;
|
_validatedInformation = information;
|
||||||
if (const auto error = localInformationError(information)) {
|
if (!validateInformationLocal(information)) {
|
||||||
_updates.fire_copy(error);
|
|
||||||
return;
|
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(
|
_validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo(
|
||||||
MTP_flags(0), // #TODO payments save information
|
MTP_flags(0), // #TODO payments save information
|
||||||
MTP_int(_msgId.msg),
|
MTP_int(_msgId.msg),
|
||||||
|
@ -415,26 +428,43 @@ void Form::validateInformation(const Ui::RequestedInformation &information) {
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Error Form::localInformationError(
|
bool Form::validateInformationLocal(
|
||||||
const Ui::RequestedInformation &information) const {
|
const Ui::RequestedInformation &information) const {
|
||||||
const auto error = [](const QString &id) {
|
if (const auto error = informationErrorLocal(information)) {
|
||||||
return Error{ Error::Type::Validate, id };
|
_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 (_invoice.isShippingAddressRequested) {
|
||||||
&& !information.shippingAddress) {
|
if (information.shippingAddress.address1.isEmpty()) {
|
||||||
return information.shippingAddress.address1.isEmpty()
|
push(u"ADDRESS_STREET_LINE1_INVALID"_q);
|
||||||
? error(u"ADDRESS_STREET_LINE1_INVALID"_q)
|
}
|
||||||
: information.shippingAddress.city.isEmpty()
|
if (information.shippingAddress.city.isEmpty()) {
|
||||||
? error(u"ADDRESS_CITY_INVALID"_q)
|
push(u"ADDRESS_CITY_INVALID"_q);
|
||||||
: information.shippingAddress.countryIso2.isEmpty()
|
}
|
||||||
? error(u"ADDRESS_COUNTRY_INVALID"_q)
|
if (information.shippingAddress.countryIso2.isEmpty()) {
|
||||||
: (Unexpected("Shipping Address error."), Error());
|
push(u"ADDRESS_COUNTRY_INVALID"_q);
|
||||||
} else if (_invoice.isNameRequested && information.name.isEmpty()) {
|
}
|
||||||
return error(u"REQ_INFO_NAME_INVALID"_q);
|
}
|
||||||
} else if (_invoice.isEmailRequested && information.email.isEmpty()) {
|
if (_invoice.isNameRequested && information.name.isEmpty()) {
|
||||||
return error(u"REQ_INFO_EMAIL_INVALID"_q);
|
push(u"REQ_INFO_NAME_INVALID"_q);
|
||||||
} else if (_invoice.isPhoneRequested && information.phone.isEmpty()) {
|
}
|
||||||
return error(u"REQ_INFO_PHONE_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();
|
return Error();
|
||||||
}
|
}
|
||||||
|
@ -442,6 +472,9 @@ Error Form::localInformationError(
|
||||||
void Form::validateCard(const Ui::UncheckedCardDetails &details) {
|
void Form::validateCard(const Ui::UncheckedCardDetails &details) {
|
||||||
Expects(!v::is_null(_paymentMethod.native.data));
|
Expects(!v::is_null(_paymentMethod.native.data));
|
||||||
|
|
||||||
|
if (!validateCardLocal(details)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto &native = _paymentMethod.native.data;
|
const auto &native = _paymentMethod.native.data;
|
||||||
if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
|
if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
|
||||||
validateCard(*stripe, details);
|
validateCard(*stripe, details);
|
||||||
|
@ -450,6 +483,52 @@ void Form::validateCard(const Ui::UncheckedCardDetails &details) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Form::validateCardLocal(const Ui::UncheckedCardDetails &details) const {
|
||||||
|
if (auto error = cardErrorLocal(details)) {
|
||||||
|
_updates.fire(std::move(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error Form::cardErrorLocal(const Ui::UncheckedCardDetails &details) const {
|
||||||
|
using namespace Stripe;
|
||||||
|
|
||||||
|
auto errors = QStringList();
|
||||||
|
const auto push = [&](const QString &id) {
|
||||||
|
errors.push_back(id);
|
||||||
|
};
|
||||||
|
const auto kValid = ValidationState::Valid;
|
||||||
|
if (ValidateCard(details.number).state != kValid) {
|
||||||
|
push(u"LOCAL_CARD_NUMBER_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (ValidateParsedExpireDate(
|
||||||
|
details.expireMonth,
|
||||||
|
details.expireYear
|
||||||
|
) != kValid) {
|
||||||
|
push(u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (ValidateCvc(details.number, details.cvc).state != kValid) {
|
||||||
|
push(u"LOCAL_CARD_CVC_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (_paymentMethod.ui.native.needCardholderName
|
||||||
|
&& details.cardholderName.isEmpty()) {
|
||||||
|
push(u"LOCAL_CARD_HOLDER_NAME_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (_paymentMethod.ui.native.needCountry
|
||||||
|
&& details.addressCountry.isEmpty()) {
|
||||||
|
push(u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (_paymentMethod.ui.native.needZip
|
||||||
|
&& details.addressZip.isEmpty()) {
|
||||||
|
push(u"LOCAL_CARD_BILLING_ZIP_INVALID"_q);
|
||||||
|
}
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return Error{ Error::Type::Validate, errors.front() };
|
||||||
|
}
|
||||||
|
return Error();
|
||||||
|
}
|
||||||
|
|
||||||
void Form::validateCard(
|
void Form::validateCard(
|
||||||
const StripePaymentMethod &method,
|
const StripePaymentMethod &method,
|
||||||
const Ui::UncheckedCardDetails &details) {
|
const Ui::UncheckedCardDetails &details) {
|
||||||
|
|
|
@ -196,14 +196,23 @@ private:
|
||||||
void fillPaymentMethodInformation();
|
void fillPaymentMethodInformation();
|
||||||
void fillStripeNativeMethod();
|
void fillStripeNativeMethod();
|
||||||
void refreshPaymentMethodDetails();
|
void refreshPaymentMethodDetails();
|
||||||
|
[[nodiscard]] QString defaultPhone() const;
|
||||||
[[nodiscard]] QString defaultCountry() const;
|
[[nodiscard]] QString defaultCountry() const;
|
||||||
|
|
||||||
void validateCard(
|
void validateCard(
|
||||||
const StripePaymentMethod &method,
|
const StripePaymentMethod &method,
|
||||||
const Ui::UncheckedCardDetails &details);
|
const Ui::UncheckedCardDetails &details);
|
||||||
|
|
||||||
[[nodiscard]] Error localInformationError(
|
bool validateInformationLocal(
|
||||||
const Ui::RequestedInformation &information) const;
|
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;
|
const not_null<Main::Session*> _session;
|
||||||
MTP::Sender _api;
|
MTP::Sender _api;
|
||||||
|
|
|
@ -20,6 +20,7 @@ enum class CardBrand {
|
||||||
Discover,
|
Discover,
|
||||||
JCB,
|
JCB,
|
||||||
DinersClub,
|
DinersClub,
|
||||||
|
UnionPay,
|
||||||
Unknown,
|
Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
279
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal file
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
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
|
|
@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
#include "payments/ui/payments_panel_delegate.h"
|
#include "payments/ui/payments_panel_delegate.h"
|
||||||
#include "payments/ui/payments_field.h"
|
#include "payments/ui/payments_field.h"
|
||||||
|
#include "stripe/stripe_card_validator.h"
|
||||||
#include "ui/widgets/scroll_area.h"
|
#include "ui/widgets/scroll_area.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/labels.h"
|
#include "ui/widgets/labels.h"
|
||||||
|
@ -18,10 +19,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "styles/style_payments.h"
|
#include "styles/style_payments.h"
|
||||||
#include "styles/style_passport.h"
|
#include "styles/style_passport.h"
|
||||||
|
|
||||||
|
#include <QtCore/QRegularExpression>
|
||||||
|
|
||||||
namespace Payments::Ui {
|
namespace Payments::Ui {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kMaxPostcodeSize = 10;
|
struct SimpleFieldState {
|
||||||
|
QString value;
|
||||||
|
int position = 0;
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] uint32 ExtractYear(const QString &value) {
|
[[nodiscard]] uint32 ExtractYear(const QString &value) {
|
||||||
return value.split('/').value(1).toInt() + 2000;
|
return value.split('/').value(1).toInt() + 2000;
|
||||||
|
@ -31,6 +37,165 @@ constexpr auto kMaxPostcodeSize = 10;
|
||||||
return value.split('/').value(0).toInt();
|
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
|
} // namespace
|
||||||
|
|
||||||
EditCard::EditCard(
|
EditCard::EditCard(
|
||||||
|
@ -111,15 +276,8 @@ not_null<RpWidget*> EditCard::setupContent() {
|
||||||
_number = add({
|
_number = add({
|
||||||
.type = FieldType::CardNumber,
|
.type = FieldType::CardNumber,
|
||||||
.placeholder = tr::lng_payments_card_number(),
|
.placeholder = tr::lng_payments_card_number(),
|
||||||
.required = true,
|
.validator = CardNumberValidator(),
|
||||||
});
|
});
|
||||||
if (_native.needCardholderName) {
|
|
||||||
_name = add({
|
|
||||||
.type = FieldType::CardNumber,
|
|
||||||
.placeholder = tr::lng_payments_card_holder(),
|
|
||||||
.required = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
auto container = inner->add(
|
auto container = inner->add(
|
||||||
object_ptr<FixedHeightWidget>(
|
object_ptr<FixedHeightWidget>(
|
||||||
inner,
|
inner,
|
||||||
|
@ -128,12 +286,12 @@ not_null<RpWidget*> EditCard::setupContent() {
|
||||||
_expire = std::make_unique<Field>(container, FieldConfig{
|
_expire = std::make_unique<Field>(container, FieldConfig{
|
||||||
.type = FieldType::CardExpireDate,
|
.type = FieldType::CardExpireDate,
|
||||||
.placeholder = rpl::single(u"MM / YY"_q),
|
.placeholder = rpl::single(u"MM / YY"_q),
|
||||||
.required = true,
|
.validator = ExpireDateValidator(),
|
||||||
});
|
});
|
||||||
_cvc = std::make_unique<Field>(container, FieldConfig{
|
_cvc = std::make_unique<Field>(container, FieldConfig{
|
||||||
.type = FieldType::CardCVC,
|
.type = FieldType::CardCVC,
|
||||||
.placeholder = rpl::single(u"CVC"_q),
|
.placeholder = rpl::single(u"CVC"_q),
|
||||||
.required = true,
|
.validator = CvcValidator([=] { return _number->value(); }),
|
||||||
});
|
});
|
||||||
container->widthValue(
|
container->widthValue(
|
||||||
) | rpl::start_with_next([=](int width) {
|
) | rpl::start_with_next([=](int width) {
|
||||||
|
@ -144,6 +302,24 @@ not_null<RpWidget*> EditCard::setupContent() {
|
||||||
_expire->widget()->moveToLeft(0, 0, width);
|
_expire->widget()->moveToLeft(0, 0, width);
|
||||||
_cvc->widget()->moveToRight(0, 0, width);
|
_cvc->widget()->moveToRight(0, 0, width);
|
||||||
}, container->lifetime());
|
}, 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) {
|
if (_native.needCountry || _native.needZip) {
|
||||||
inner->add(
|
inner->add(
|
||||||
object_ptr<Ui::FlatLabel>(
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
@ -156,18 +332,23 @@ not_null<RpWidget*> EditCard::setupContent() {
|
||||||
_country = add({
|
_country = add({
|
||||||
.type = FieldType::Country,
|
.type = FieldType::Country,
|
||||||
.placeholder = tr::lng_payments_billing_country(),
|
.placeholder = tr::lng_payments_billing_country(),
|
||||||
|
.validator = RequiredFinishedValidator(),
|
||||||
.showBox = showBox,
|
.showBox = showBox,
|
||||||
.defaultCountry = _native.defaultCountry,
|
.defaultCountry = _native.defaultCountry,
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_native.needZip) {
|
if (_native.needZip) {
|
||||||
_zip = add({
|
_zip = add({
|
||||||
.type = FieldType::Text,
|
.type = FieldType::Text,
|
||||||
.placeholder = tr::lng_payments_billing_zip_code(),
|
.placeholder = tr::lng_payments_billing_zip_code(),
|
||||||
.maxLength = kMaxPostcodeSize,
|
.validator = RequiredValidator(),
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
|
if (_country) {
|
||||||
|
_country->finished(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
_zip->setFocus();
|
||||||
|
}, lifetime());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return inner;
|
return inner;
|
||||||
}
|
}
|
||||||
|
@ -198,7 +379,7 @@ void EditCard::updateControlsGeometry() {
|
||||||
auto EditCard::lookupField(CardField field) const -> Field* {
|
auto EditCard::lookupField(CardField field) const -> Field* {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case CardField::Number: return _number.get();
|
case CardField::Number: return _number.get();
|
||||||
case CardField::CVC: return _cvc.get();
|
case CardField::Cvc: return _cvc.get();
|
||||||
case CardField::ExpireDate: return _expire.get();
|
case CardField::ExpireDate: return _expire.get();
|
||||||
case CardField::Name: return _name.get();
|
case CardField::Name: return _name.get();
|
||||||
case CardField::AddressCountry: return _country.get();
|
case CardField::AddressCountry: return _country.get();
|
||||||
|
|
|
@ -26,6 +26,8 @@ constexpr auto kMaxPostcodeSize = 10;
|
||||||
constexpr auto kMaxNameSize = 64;
|
constexpr auto kMaxNameSize = 64;
|
||||||
constexpr auto kMaxEmailSize = 128;
|
constexpr auto kMaxEmailSize = 128;
|
||||||
constexpr auto kMaxPhoneSize = 16;
|
constexpr auto kMaxPhoneSize = 16;
|
||||||
|
constexpr auto kMinCitySize = 2;
|
||||||
|
constexpr auto kMaxCitySize = 64;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -112,18 +114,17 @@ not_null<RpWidget*> EditInformation::setupContent() {
|
||||||
_street1 = add({
|
_street1 = add({
|
||||||
.placeholder = tr::lng_payments_address_street1(),
|
.placeholder = tr::lng_payments_address_street1(),
|
||||||
.value = _information.shippingAddress.address1,
|
.value = _information.shippingAddress.address1,
|
||||||
.maxLength = kMaxStreetSize,
|
.validator = RangeLengthValidator(1, kMaxStreetSize),
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
_street2 = add({
|
_street2 = add({
|
||||||
.placeholder = tr::lng_payments_address_street2(),
|
.placeholder = tr::lng_payments_address_street2(),
|
||||||
.value = _information.shippingAddress.address2,
|
.value = _information.shippingAddress.address2,
|
||||||
.maxLength = kMaxStreetSize,
|
.validator = MaxLengthValidator(kMaxStreetSize),
|
||||||
});
|
});
|
||||||
_city = add({
|
_city = add({
|
||||||
.placeholder = tr::lng_payments_address_city(),
|
.placeholder = tr::lng_payments_address_city(),
|
||||||
.value = _information.shippingAddress.city,
|
.value = _information.shippingAddress.city,
|
||||||
.required = true,
|
.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),
|
||||||
});
|
});
|
||||||
_state = add({
|
_state = add({
|
||||||
.placeholder = tr::lng_payments_address_state(),
|
.placeholder = tr::lng_payments_address_state(),
|
||||||
|
@ -133,44 +134,38 @@ not_null<RpWidget*> EditInformation::setupContent() {
|
||||||
.type = FieldType::Country,
|
.type = FieldType::Country,
|
||||||
.placeholder = tr::lng_payments_address_country(),
|
.placeholder = tr::lng_payments_address_country(),
|
||||||
.value = _information.shippingAddress.countryIso2,
|
.value = _information.shippingAddress.countryIso2,
|
||||||
|
.validator = RequiredFinishedValidator(),
|
||||||
.showBox = showBox,
|
.showBox = showBox,
|
||||||
.defaultCountry = _information.defaultCountry,
|
.defaultCountry = _information.defaultCountry,
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
_postcode = add({
|
_postcode = add({
|
||||||
.placeholder = tr::lng_payments_address_postcode(),
|
.placeholder = tr::lng_payments_address_postcode(),
|
||||||
.value = _information.shippingAddress.postcode,
|
.value = _information.shippingAddress.postcode,
|
||||||
.maxLength = kMaxPostcodeSize,
|
.validator = RangeLengthValidator(1, kMaxPostcodeSize),
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
//StreetValidate, // #TODO payments
|
|
||||||
//CityValidate,
|
|
||||||
//CountryValidate,
|
|
||||||
//CountryFormat,
|
|
||||||
//PostcodeValidate,
|
|
||||||
}
|
}
|
||||||
if (_invoice.isNameRequested) {
|
if (_invoice.isNameRequested) {
|
||||||
_name = add({
|
_name = add({
|
||||||
.placeholder = tr::lng_payments_info_name(),
|
.placeholder = tr::lng_payments_info_name(),
|
||||||
.value = _information.name,
|
.value = _information.name,
|
||||||
.maxLength = kMaxNameSize,
|
.validator = RangeLengthValidator(1, kMaxNameSize),
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_invoice.isEmailRequested) {
|
if (_invoice.isEmailRequested) {
|
||||||
_email = add({
|
_email = add({
|
||||||
|
.type = FieldType::Email,
|
||||||
.placeholder = tr::lng_payments_info_email(),
|
.placeholder = tr::lng_payments_info_email(),
|
||||||
.value = _information.email,
|
.value = _information.email,
|
||||||
.maxLength = kMaxEmailSize,
|
.validator = RangeLengthValidator(1, kMaxEmailSize),
|
||||||
.required = true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_invoice.isPhoneRequested) {
|
if (_invoice.isPhoneRequested) {
|
||||||
_phone = add({
|
_phone = add({
|
||||||
|
.type = FieldType::Phone,
|
||||||
.placeholder = tr::lng_payments_info_phone(),
|
.placeholder = tr::lng_payments_info_phone(),
|
||||||
.value = _information.phone,
|
.value = _information.phone,
|
||||||
.maxLength = kMaxPhoneSize,
|
.validator = RangeLengthValidator(1, kMaxPhoneSize),
|
||||||
.required = true,
|
.defaultPhone = _information.defaultPhone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return inner;
|
return inner;
|
||||||
|
@ -215,6 +210,8 @@ auto EditInformation::lookupField(InformationField field) const -> Field* {
|
||||||
|
|
||||||
RequestedInformation EditInformation::collect() const {
|
RequestedInformation EditInformation::collect() const {
|
||||||
return {
|
return {
|
||||||
|
.defaultPhone = _information.defaultPhone,
|
||||||
|
.defaultCountry = _information.defaultCountry,
|
||||||
.name = _name ? _name->value() : QString(),
|
.name = _name ? _name->value() : QString(),
|
||||||
.phone = _phone ? _phone->value() : QString(),
|
.phone = _phone ? _phone->value() : QString(),
|
||||||
.email = _email ? _email->value() : QString(),
|
.email = _email ? _email->value() : QString(),
|
||||||
|
|
|
@ -9,8 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
#include "ui/widgets/input_fields.h"
|
#include "ui/widgets/input_fields.h"
|
||||||
#include "ui/boxes/country_select_box.h"
|
#include "ui/boxes/country_select_box.h"
|
||||||
|
#include "ui/ui_utility.h"
|
||||||
|
#include "ui/special_fields.h"
|
||||||
#include "data/data_countries.h"
|
#include "data/data_countries.h"
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
|
#include "base/event_filter.h"
|
||||||
#include "styles/style_payments.h"
|
#include "styles/style_payments.h"
|
||||||
|
|
||||||
namespace Payments::Ui {
|
namespace Payments::Ui {
|
||||||
|
@ -91,12 +94,18 @@ namespace {
|
||||||
case FieldType::CardExpireDate:
|
case FieldType::CardExpireDate:
|
||||||
case FieldType::CardCVC:
|
case FieldType::CardCVC:
|
||||||
case FieldType::Country:
|
case FieldType::Country:
|
||||||
case FieldType::Phone:
|
|
||||||
return CreateChild<MaskedInputField>(
|
return CreateChild<MaskedInputField>(
|
||||||
wrap.get(),
|
wrap.get(),
|
||||||
st::paymentsField,
|
st::paymentsField,
|
||||||
std::move(config.placeholder),
|
std::move(config.placeholder),
|
||||||
Parse(config));
|
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.");
|
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
|
||||||
}
|
}
|
||||||
|
@ -115,6 +124,10 @@ Field::Field(QWidget *parent, FieldConfig &&config)
|
||||||
if (_config.type == FieldType::Country) {
|
if (_config.type == FieldType::Country) {
|
||||||
setupCountry();
|
setupCountry();
|
||||||
}
|
}
|
||||||
|
if (const auto &validator = config.validator) {
|
||||||
|
setupValidator(validator);
|
||||||
|
}
|
||||||
|
setupFrontBackspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
RpWidget *Field::widget() const {
|
RpWidget *Field::widget() const {
|
||||||
|
@ -132,6 +145,14 @@ QString Field::value() const {
|
||||||
_countryIso2);
|
_countryIso2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpl::producer<> Field::frontBackspace() const {
|
||||||
|
return _frontBackspace.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> Field::finished() const {
|
||||||
|
return _finished.events();
|
||||||
|
}
|
||||||
|
|
||||||
void Field::setupMaskedGeometry() {
|
void Field::setupMaskedGeometry() {
|
||||||
Expects(_masked != nullptr);
|
Expects(_masked != nullptr);
|
||||||
|
|
||||||
|
@ -168,13 +189,136 @@ void Field::setupCountry() {
|
||||||
_countryIso2 = iso2;
|
_countryIso2 = iso2;
|
||||||
_masked->setText(Data::CountryNameByISO2(iso2));
|
_masked->setText(Data::CountryNameByISO2(iso2));
|
||||||
_masked->hideError();
|
_masked->hideError();
|
||||||
setFocus();
|
|
||||||
raw->closeBox();
|
raw->closeBox();
|
||||||
}, _masked->lifetime());
|
}, _masked->lifetime());
|
||||||
|
raw->boxClosing() | rpl::start_with_next([=] {
|
||||||
|
setFocus();
|
||||||
|
}, _masked->lifetime());
|
||||||
_config.showBox(std::move(box));
|
_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() {
|
void Field::setFocus() {
|
||||||
if (_config.type == FieldType::Country) {
|
if (_config.type == FieldType::Country) {
|
||||||
_wrap->setFocus();
|
_wrap->setFocus();
|
||||||
|
@ -206,4 +350,12 @@ void Field::showError() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Field::showErrorNoFocus() {
|
||||||
|
if (_input) {
|
||||||
|
_input->showErrorNoFocus();
|
||||||
|
} else {
|
||||||
|
_masked->showErrorNoFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Payments::Ui
|
} // namespace Payments::Ui
|
||||||
|
|
|
@ -31,14 +31,59 @@ enum class FieldType {
|
||||||
Email,
|
Email,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FieldValidateRequest {
|
||||||
|
QString wasValue;
|
||||||
|
int wasPosition = 0;
|
||||||
|
int wasAnchor = 0;
|
||||||
|
QString nowValue;
|
||||||
|
int nowPosition = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldValidateResult {
|
||||||
|
QString value;
|
||||||
|
int position = 0;
|
||||||
|
bool invalid = false;
|
||||||
|
bool finished = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto RangeLengthValidator(int minLength, int maxLength) {
|
||||||
|
return [=](FieldValidateRequest request) {
|
||||||
|
return FieldValidateResult{
|
||||||
|
.value = request.nowValue,
|
||||||
|
.position = request.nowPosition,
|
||||||
|
.invalid = (request.nowValue.size() < minLength
|
||||||
|
|| request.nowValue.size() > maxLength),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto MaxLengthValidator(int maxLength) {
|
||||||
|
return RangeLengthValidator(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto RequiredValidator() {
|
||||||
|
return RangeLengthValidator(1, std::numeric_limits<int>::max());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto RequiredFinishedValidator() {
|
||||||
|
return [=](FieldValidateRequest request) {
|
||||||
|
return FieldValidateResult{
|
||||||
|
.value = request.nowValue,
|
||||||
|
.position = request.nowPosition,
|
||||||
|
.invalid = request.nowValue.isEmpty(),
|
||||||
|
.finished = !request.nowValue.isEmpty(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
struct FieldConfig {
|
struct FieldConfig {
|
||||||
FieldType type = FieldType::Text;
|
FieldType type = FieldType::Text;
|
||||||
rpl::producer<QString> placeholder;
|
rpl::producer<QString> placeholder;
|
||||||
QString value;
|
QString value;
|
||||||
|
Fn<FieldValidateResult(FieldValidateRequest)> validator;
|
||||||
Fn<void(object_ptr<BoxContent>)> showBox;
|
Fn<void(object_ptr<BoxContent>)> showBox;
|
||||||
|
QString defaultPhone;
|
||||||
QString defaultCountry;
|
QString defaultCountry;
|
||||||
int maxLength = 0;
|
|
||||||
bool required = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Field final {
|
class Field final {
|
||||||
|
@ -49,20 +94,40 @@ public:
|
||||||
[[nodiscard]] object_ptr<RpWidget> ownedWidget() const;
|
[[nodiscard]] object_ptr<RpWidget> ownedWidget() const;
|
||||||
|
|
||||||
[[nodiscard]] QString value() const;
|
[[nodiscard]] QString value() const;
|
||||||
|
[[nodiscard]] rpl::producer<> frontBackspace() const;
|
||||||
|
[[nodiscard]] rpl::producer<> finished() const;
|
||||||
|
|
||||||
void setFocus();
|
void setFocus();
|
||||||
void setFocusFast();
|
void setFocusFast();
|
||||||
void showError();
|
void showError();
|
||||||
|
void showErrorNoFocus();
|
||||||
|
|
||||||
|
void setNextField(not_null<Field*> field);
|
||||||
|
void setPreviousField(not_null<Field*> field);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct State {
|
||||||
|
QString value;
|
||||||
|
int position = 0;
|
||||||
|
int anchor = 0;
|
||||||
|
};
|
||||||
|
using ValidateRequest = FieldValidateRequest;
|
||||||
|
using ValidateResult = FieldValidateResult;
|
||||||
|
|
||||||
void setupMaskedGeometry();
|
void setupMaskedGeometry();
|
||||||
void setupCountry();
|
void setupCountry();
|
||||||
|
void setupValidator(Fn<ValidateResult(ValidateRequest)> validator);
|
||||||
|
void setupFrontBackspace();
|
||||||
|
|
||||||
const FieldConfig _config;
|
const FieldConfig _config;
|
||||||
const base::unique_qptr<RpWidget> _wrap;
|
const base::unique_qptr<RpWidget> _wrap;
|
||||||
|
rpl::event_stream<> _frontBackspace;
|
||||||
|
rpl::event_stream<> _finished;
|
||||||
InputField *_input = nullptr;
|
InputField *_input = nullptr;
|
||||||
MaskedInputField *_masked = nullptr;
|
MaskedInputField *_masked = nullptr;
|
||||||
QString _countryIso2;
|
QString _countryIso2;
|
||||||
|
State _was;
|
||||||
|
bool _validating = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/wrap/vertical_layout.h"
|
#include "ui/wrap/vertical_layout.h"
|
||||||
#include "ui/wrap/fade_wrap.h"
|
#include "ui/wrap/fade_wrap.h"
|
||||||
#include "ui/text/format_values.h"
|
#include "ui/text/format_values.h"
|
||||||
|
#include "data/data_countries.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "styles/style_payments.h"
|
#include "styles/style_payments.h"
|
||||||
#include "styles/style_passport.h"
|
#include "styles/style_passport.h"
|
||||||
|
@ -267,7 +268,8 @@ void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
|
||||||
push(_information.shippingAddress.address2);
|
push(_information.shippingAddress.address2);
|
||||||
push(_information.shippingAddress.city);
|
push(_information.shippingAddress.city);
|
||||||
push(_information.shippingAddress.state);
|
push(_information.shippingAddress.state);
|
||||||
push(_information.shippingAddress.countryIso2);
|
push(Data::CountryNameByISO2(
|
||||||
|
_information.shippingAddress.countryIso2));
|
||||||
push(_information.shippingAddress.postcode);
|
push(_information.shippingAddress.postcode);
|
||||||
add(
|
add(
|
||||||
tr::lng_payments_shipping_address(),
|
tr::lng_payments_shipping_address(),
|
||||||
|
|
|
@ -87,6 +87,7 @@ struct Address {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RequestedInformation {
|
struct RequestedInformation {
|
||||||
|
QString defaultPhone;
|
||||||
QString defaultCountry;
|
QString defaultCountry;
|
||||||
|
|
||||||
QString name;
|
QString name;
|
||||||
|
@ -144,7 +145,7 @@ struct PaymentMethodDetails {
|
||||||
|
|
||||||
enum class CardField {
|
enum class CardField {
|
||||||
Number,
|
Number,
|
||||||
CVC,
|
Cvc,
|
||||||
ExpireDate,
|
ExpireDate,
|
||||||
Name,
|
Name,
|
||||||
AddressCountry,
|
AddressCountry,
|
||||||
|
|
|
@ -28,7 +28,7 @@ QString LastValidISO;
|
||||||
|
|
||||||
class CountrySelectBox::Inner : public TWidget {
|
class CountrySelectBox::Inner : public TWidget {
|
||||||
public:
|
public:
|
||||||
Inner(QWidget *parent, Type type);
|
Inner(QWidget *parent, const QString &iso, Type type);
|
||||||
~Inner();
|
~Inner();
|
||||||
|
|
||||||
void updateFilter(QString filter = QString());
|
void updateFilter(QString filter = QString());
|
||||||
|
@ -93,10 +93,7 @@ CountrySelectBox::CountrySelectBox(QWidget*)
|
||||||
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
|
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
|
||||||
: _type(type)
|
: _type(type)
|
||||||
, _select(this, st::defaultMultiSelect, tr::lng_country_ph())
|
, _select(this, st::defaultMultiSelect, tr::lng_country_ph())
|
||||||
, _ownedInner(this, type) {
|
, _ownedInner(this, iso, type) {
|
||||||
if (Data::CountriesByISO2().contains(iso)) {
|
|
||||||
LastValidISO = iso;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<QString> CountrySelectBox::countryChosen() const {
|
rpl::producer<QString> CountrySelectBox::countryChosen() const {
|
||||||
|
@ -169,7 +166,10 @@ void CountrySelectBox::setInnerFocus() {
|
||||||
_select->setInnerFocus();
|
_select->setInnerFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
CountrySelectBox::Inner::Inner(QWidget *parent, Type type)
|
CountrySelectBox::Inner::Inner(
|
||||||
|
QWidget *parent,
|
||||||
|
const QString &iso,
|
||||||
|
Type type)
|
||||||
: TWidget(parent)
|
: TWidget(parent)
|
||||||
, _type(type)
|
, _type(type)
|
||||||
, _rowHeight(st::countryRowHeight) {
|
, _rowHeight(st::countryRowHeight) {
|
||||||
|
@ -177,6 +177,10 @@ CountrySelectBox::Inner::Inner(QWidget *parent, Type type)
|
||||||
|
|
||||||
const auto &byISO2 = Data::CountriesByISO2();
|
const auto &byISO2 = Data::CountriesByISO2();
|
||||||
|
|
||||||
|
if (byISO2.contains(iso)) {
|
||||||
|
LastValidISO = iso;
|
||||||
|
}
|
||||||
|
|
||||||
_list.reserve(byISO2.size());
|
_list.reserve(byISO2.size());
|
||||||
_namesList.reserve(byISO2.size());
|
_namesList.reserve(byISO2.size());
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "ui/special_fields.h"
|
#include "ui/special_fields.h"
|
||||||
|
|
||||||
#include "core/application.h"
|
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "data/data_countries.h" // Data::ValidPhoneCode
|
#include "data/data_countries.h" // Data::ValidPhoneCode
|
||||||
#include "numbers.h"
|
#include "numbers.h"
|
||||||
|
|
||||||
|
#include <QtCore/QRegularExpression>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kMaxUsernameLength = 32;
|
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
|
} // namespace
|
||||||
|
|
||||||
CountryCodeInput::CountryCodeInput(
|
CountryCodeInput::CountryCodeInput(
|
||||||
|
@ -130,7 +138,9 @@ void PhonePartInput::correctValue(
|
||||||
++digitCount;
|
++digitCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (digitCount > MaxPhoneTailLength) digitCount = MaxPhoneTailLength;
|
if (digitCount > kMaxPhoneTailLength) {
|
||||||
|
digitCount = kMaxPhoneTailLength;
|
||||||
|
}
|
||||||
|
|
||||||
bool inPart = !_pattern.isEmpty();
|
bool inPart = !_pattern.isEmpty();
|
||||||
int curPart = -1, leftInPart = 0;
|
int curPart = -1, leftInPart = 0;
|
||||||
|
@ -273,6 +283,14 @@ void UsernameInput::correctValue(
|
||||||
setCorrectedText(now, nowCursor, now.mid(from, len), newPos);
|
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(
|
PhoneInput::PhoneInput(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
const style::InputField &st,
|
const style::InputField &st,
|
||||||
|
@ -324,7 +342,7 @@ void PhoneInput::correctValue(
|
||||||
QString &now,
|
QString &now,
|
||||||
int &nowCursor) {
|
int &nowCursor) {
|
||||||
auto digits = now;
|
auto digits = now;
|
||||||
digits.replace(QRegularExpression(qsl("[^\\d]")), QString());
|
digits.replace(QRegularExpression("[^\\d]"), QString());
|
||||||
_pattern = phoneNumberParse(digits);
|
_pattern = phoneNumberParse(digits);
|
||||||
|
|
||||||
QString newPlaceholder;
|
QString newPlaceholder;
|
||||||
|
@ -350,7 +368,7 @@ void PhoneInput::correctValue(
|
||||||
}
|
}
|
||||||
|
|
||||||
QString newText;
|
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;
|
bool inPart = !_pattern.isEmpty(), plusFound = false;
|
||||||
int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;
|
int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;
|
||||||
|
|
|
@ -90,6 +90,8 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] QString ExtractPhonePrefix(const QString &phone);
|
||||||
|
|
||||||
class PhoneInput : public MaskedInputField {
|
class PhoneInput : public MaskedInputField {
|
||||||
public:
|
public:
|
||||||
PhoneInput(
|
PhoneInput(
|
||||||
|
|
|
@ -21,6 +21,8 @@ PRIVATE
|
||||||
stripe/stripe_card.h
|
stripe/stripe_card.h
|
||||||
stripe/stripe_card_params.cpp
|
stripe/stripe_card_params.cpp
|
||||||
stripe/stripe_card_params.h
|
stripe/stripe_card_params.h
|
||||||
|
stripe/stripe_card_validator.cpp
|
||||||
|
stripe/stripe_card_validator.h
|
||||||
stripe/stripe_decode.cpp
|
stripe/stripe_decode.cpp
|
||||||
stripe/stripe_decode.h
|
stripe/stripe_decode.h
|
||||||
stripe/stripe_error.cpp
|
stripe/stripe_error.cpp
|
||||||
|
|
|
@ -146,7 +146,9 @@ PRIVATE
|
||||||
ui/cached_round_corners.h
|
ui/cached_round_corners.h
|
||||||
ui/grouped_layout.cpp
|
ui/grouped_layout.cpp
|
||||||
ui/grouped_layout.h
|
ui/grouped_layout.h
|
||||||
|
ui/special_fields.cpp
|
||||||
|
ui/special_fields.h
|
||||||
|
|
||||||
ui/ui_pch.h
|
ui/ui_pch.h
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -163,4 +165,5 @@ PUBLIC
|
||||||
PRIVATE
|
PRIVATE
|
||||||
desktop-app::lib_ffmpeg
|
desktop-app::lib_ffmpeg
|
||||||
desktop-app::lib_webview
|
desktop-app::lib_webview
|
||||||
|
desktop-app::lib_stripe
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue