Add local validation for card information.
This commit is contained in:
parent
e077163322
commit
0af6c4b0b6
|
@ -1029,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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -166,23 +166,36 @@ void CheckoutProcess::handleError(const Error &error) {
|
|||
showForm();
|
||||
return;
|
||||
}
|
||||
using Field = Ui::InformationField;
|
||||
using InfoField = Ui::InformationField;
|
||||
using CardField = Ui::CardField;
|
||||
if (id == u"REQ_INFO_NAME_INVALID"_q) {
|
||||
showInformationError(Field::Name);
|
||||
showInformationError(InfoField::Name);
|
||||
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
|
||||
showInformationError(Field::Email);
|
||||
showInformationError(InfoField::Email);
|
||||
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
|
||||
showInformationError(Field::Phone);
|
||||
showInformationError(InfoField::Phone);
|
||||
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
|
||||
showInformationError(Field::ShippingStreet);
|
||||
showInformationError(InfoField::ShippingStreet);
|
||||
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
|
||||
showInformationError(Field::ShippingCity);
|
||||
showInformationError(InfoField::ShippingCity);
|
||||
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
|
||||
showInformationError(Field::ShippingState);
|
||||
showInformationError(InfoField::ShippingState);
|
||||
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
|
||||
showInformationError(Field::ShippingCountry);
|
||||
showInformationError(InfoField::ShippingCountry);
|
||||
} 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) {
|
||||
showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message
|
||||
} 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) {
|
||||
showCardError(Field::Number);
|
||||
} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
|
||||
showCardError(Field::CVC);
|
||||
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 });
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ 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_card_validator.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_payments.h" // paymentsThumbnailSize.
|
||||
|
@ -270,6 +271,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) {
|
|||
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()),
|
||||
|
@ -293,11 +295,16 @@ void Form::refreshPaymentMethodDetails() {
|
|||
const auto &entered = _paymentMethod.newCredentials;
|
||||
_paymentMethod.ui.title = entered ? entered.title : saved.title;
|
||||
_paymentMethod.ui.ready = entered || saved;
|
||||
_paymentMethod.ui.native.defaultPhone = defaultPhone();
|
||||
_paymentMethod.ui.native.defaultCountry = defaultCountry();
|
||||
}
|
||||
|
||||
QString Form::defaultPhone() const {
|
||||
return _session->user()->phone();
|
||||
}
|
||||
|
||||
QString Form::defaultCountry() const {
|
||||
return Data::CountryISO2ByPhone(_session->user()->phone());
|
||||
return Data::CountryISO2ByPhone(defaultPhone());
|
||||
}
|
||||
|
||||
void Form::fillPaymentMethodInformation() {
|
||||
|
@ -382,10 +389,16 @@ void Form::validateInformation(const Ui::RequestedInformation &information) {
|
|||
_api.request(base::take(_validateRequestId)).cancel();
|
||||
}
|
||||
_validatedInformation = information;
|
||||
if (const auto error = localInformationError(information)) {
|
||||
_updates.fire_copy(error);
|
||||
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),
|
||||
|
@ -415,26 +428,43 @@ void Form::validateInformation(const Ui::RequestedInformation &information) {
|
|||
}).send();
|
||||
}
|
||||
|
||||
Error Form::localInformationError(
|
||||
bool Form::validateInformationLocal(
|
||||
const Ui::RequestedInformation &information) const {
|
||||
const auto error = [](const QString &id) {
|
||||
return Error{ Error::Type::Validate, id };
|
||||
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
|
||||
&& !information.shippingAddress) {
|
||||
return information.shippingAddress.address1.isEmpty()
|
||||
? error(u"ADDRESS_STREET_LINE1_INVALID"_q)
|
||||
: information.shippingAddress.city.isEmpty()
|
||||
? error(u"ADDRESS_CITY_INVALID"_q)
|
||||
: information.shippingAddress.countryIso2.isEmpty()
|
||||
? error(u"ADDRESS_COUNTRY_INVALID"_q)
|
||||
: (Unexpected("Shipping Address error."), Error());
|
||||
} else if (_invoice.isNameRequested && information.name.isEmpty()) {
|
||||
return error(u"REQ_INFO_NAME_INVALID"_q);
|
||||
} else if (_invoice.isEmailRequested && information.email.isEmpty()) {
|
||||
return error(u"REQ_INFO_EMAIL_INVALID"_q);
|
||||
} else if (_invoice.isPhoneRequested && information.phone.isEmpty()) {
|
||||
return error(u"REQ_INFO_PHONE_INVALID"_q);
|
||||
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();
|
||||
}
|
||||
|
@ -442,6 +472,9 @@ Error Form::localInformationError(
|
|||
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);
|
||||
|
@ -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(
|
||||
const StripePaymentMethod &method,
|
||||
const Ui::UncheckedCardDetails &details) {
|
||||
|
|
|
@ -196,14 +196,23 @@ private:
|
|||
void fillPaymentMethodInformation();
|
||||
void fillStripeNativeMethod();
|
||||
void refreshPaymentMethodDetails();
|
||||
[[nodiscard]] QString defaultPhone() const;
|
||||
[[nodiscard]] QString defaultCountry() const;
|
||||
|
||||
void validateCard(
|
||||
const StripePaymentMethod &method,
|
||||
const Ui::UncheckedCardDetails &details);
|
||||
|
||||
[[nodiscard]] Error localInformationError(
|
||||
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;
|
||||
|
|
|
@ -20,6 +20,7 @@ enum class CardBrand {
|
|||
Discover,
|
||||
JCB,
|
||||
DinersClub,
|
||||
UnionPay,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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_field.h"
|
||||
#include "stripe/stripe_card_validator.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/buttons.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_passport.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxPostcodeSize = 10;
|
||||
struct SimpleFieldState {
|
||||
QString value;
|
||||
int position = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint32 ExtractYear(const QString &value) {
|
||||
return value.split('/').value(1).toInt() + 2000;
|
||||
|
@ -31,6 +37,165 @@ constexpr auto kMaxPostcodeSize = 10;
|
|||
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(
|
||||
|
@ -111,15 +276,8 @@ not_null<RpWidget*> EditCard::setupContent() {
|
|||
_number = add({
|
||||
.type = FieldType::CardNumber,
|
||||
.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(
|
||||
object_ptr<FixedHeightWidget>(
|
||||
inner,
|
||||
|
@ -128,12 +286,12 @@ not_null<RpWidget*> EditCard::setupContent() {
|
|||
_expire = std::make_unique<Field>(container, FieldConfig{
|
||||
.type = FieldType::CardExpireDate,
|
||||
.placeholder = rpl::single(u"MM / YY"_q),
|
||||
.required = true,
|
||||
.validator = ExpireDateValidator(),
|
||||
});
|
||||
_cvc = std::make_unique<Field>(container, FieldConfig{
|
||||
.type = FieldType::CardCVC,
|
||||
.placeholder = rpl::single(u"CVC"_q),
|
||||
.required = true,
|
||||
.validator = CvcValidator([=] { return _number->value(); }),
|
||||
});
|
||||
container->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
|
@ -144,6 +302,24 @@ not_null<RpWidget*> EditCard::setupContent() {
|
|||
_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>(
|
||||
|
@ -156,18 +332,23 @@ not_null<RpWidget*> EditCard::setupContent() {
|
|||
_country = add({
|
||||
.type = FieldType::Country,
|
||||
.placeholder = tr::lng_payments_billing_country(),
|
||||
.validator = RequiredFinishedValidator(),
|
||||
.showBox = showBox,
|
||||
.defaultCountry = _native.defaultCountry,
|
||||
.required = true,
|
||||
});
|
||||
}
|
||||
if (_native.needZip) {
|
||||
_zip = add({
|
||||
.type = FieldType::Text,
|
||||
.placeholder = tr::lng_payments_billing_zip_code(),
|
||||
.maxLength = kMaxPostcodeSize,
|
||||
.required = true,
|
||||
.validator = RequiredValidator(),
|
||||
});
|
||||
if (_country) {
|
||||
_country->finished(
|
||||
) | rpl::start_with_next([=] {
|
||||
_zip->setFocus();
|
||||
}, lifetime());
|
||||
}
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
|
@ -198,7 +379,7 @@ void EditCard::updateControlsGeometry() {
|
|||
auto EditCard::lookupField(CardField field) const -> Field* {
|
||||
switch (field) {
|
||||
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::Name: return _name.get();
|
||||
case CardField::AddressCountry: return _country.get();
|
||||
|
|
|
@ -26,6 +26,8 @@ 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
|
||||
|
||||
|
@ -112,18 +114,17 @@ not_null<RpWidget*> EditInformation::setupContent() {
|
|||
_street1 = add({
|
||||
.placeholder = tr::lng_payments_address_street1(),
|
||||
.value = _information.shippingAddress.address1,
|
||||
.maxLength = kMaxStreetSize,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(1, kMaxStreetSize),
|
||||
});
|
||||
_street2 = add({
|
||||
.placeholder = tr::lng_payments_address_street2(),
|
||||
.value = _information.shippingAddress.address2,
|
||||
.maxLength = kMaxStreetSize,
|
||||
.validator = MaxLengthValidator(kMaxStreetSize),
|
||||
});
|
||||
_city = add({
|
||||
.placeholder = tr::lng_payments_address_city(),
|
||||
.value = _information.shippingAddress.city,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),
|
||||
});
|
||||
_state = add({
|
||||
.placeholder = tr::lng_payments_address_state(),
|
||||
|
@ -133,44 +134,38 @@ not_null<RpWidget*> EditInformation::setupContent() {
|
|||
.type = FieldType::Country,
|
||||
.placeholder = tr::lng_payments_address_country(),
|
||||
.value = _information.shippingAddress.countryIso2,
|
||||
.validator = RequiredFinishedValidator(),
|
||||
.showBox = showBox,
|
||||
.defaultCountry = _information.defaultCountry,
|
||||
.required = true,
|
||||
});
|
||||
_postcode = add({
|
||||
.placeholder = tr::lng_payments_address_postcode(),
|
||||
.value = _information.shippingAddress.postcode,
|
||||
.maxLength = kMaxPostcodeSize,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(1, kMaxPostcodeSize),
|
||||
});
|
||||
//StreetValidate, // #TODO payments
|
||||
//CityValidate,
|
||||
//CountryValidate,
|
||||
//CountryFormat,
|
||||
//PostcodeValidate,
|
||||
}
|
||||
if (_invoice.isNameRequested) {
|
||||
_name = add({
|
||||
.placeholder = tr::lng_payments_info_name(),
|
||||
.value = _information.name,
|
||||
.maxLength = kMaxNameSize,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(1, kMaxNameSize),
|
||||
});
|
||||
}
|
||||
if (_invoice.isEmailRequested) {
|
||||
_email = add({
|
||||
.type = FieldType::Email,
|
||||
.placeholder = tr::lng_payments_info_email(),
|
||||
.value = _information.email,
|
||||
.maxLength = kMaxEmailSize,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(1, kMaxEmailSize),
|
||||
});
|
||||
}
|
||||
if (_invoice.isPhoneRequested) {
|
||||
_phone = add({
|
||||
.type = FieldType::Phone,
|
||||
.placeholder = tr::lng_payments_info_phone(),
|
||||
.value = _information.phone,
|
||||
.maxLength = kMaxPhoneSize,
|
||||
.required = true,
|
||||
.validator = RangeLengthValidator(1, kMaxPhoneSize),
|
||||
.defaultPhone = _information.defaultPhone,
|
||||
});
|
||||
}
|
||||
return inner;
|
||||
|
@ -215,6 +210,8 @@ auto EditInformation::lookupField(InformationField field) const -> Field* {
|
|||
|
||||
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(),
|
||||
|
|
|
@ -9,8 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#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 {
|
||||
|
@ -91,12 +94,18 @@ namespace {
|
|||
case FieldType::CardExpireDate:
|
||||
case FieldType::CardCVC:
|
||||
case FieldType::Country:
|
||||
case FieldType::Phone:
|
||||
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.");
|
||||
}
|
||||
|
@ -115,6 +124,10 @@ Field::Field(QWidget *parent, FieldConfig &&config)
|
|||
if (_config.type == FieldType::Country) {
|
||||
setupCountry();
|
||||
}
|
||||
if (const auto &validator = config.validator) {
|
||||
setupValidator(validator);
|
||||
}
|
||||
setupFrontBackspace();
|
||||
}
|
||||
|
||||
RpWidget *Field::widget() const {
|
||||
|
@ -132,6 +145,14 @@ QString Field::value() const {
|
|||
_countryIso2);
|
||||
}
|
||||
|
||||
rpl::producer<> Field::frontBackspace() const {
|
||||
return _frontBackspace.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Field::finished() const {
|
||||
return _finished.events();
|
||||
}
|
||||
|
||||
void Field::setupMaskedGeometry() {
|
||||
Expects(_masked != nullptr);
|
||||
|
||||
|
@ -168,13 +189,136 @@ void Field::setupCountry() {
|
|||
_countryIso2 = iso2;
|
||||
_masked->setText(Data::CountryNameByISO2(iso2));
|
||||
_masked->hideError();
|
||||
setFocus();
|
||||
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();
|
||||
|
@ -206,4 +350,12 @@ void Field::showError() {
|
|||
}
|
||||
}
|
||||
|
||||
void Field::showErrorNoFocus() {
|
||||
if (_input) {
|
||||
_input->showErrorNoFocus();
|
||||
} else {
|
||||
_masked->showErrorNoFocus();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
|
|
|
@ -31,14 +31,59 @@ enum class FieldType {
|
|||
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 {
|
||||
FieldType type = FieldType::Text;
|
||||
rpl::producer<QString> placeholder;
|
||||
QString value;
|
||||
Fn<FieldValidateResult(FieldValidateRequest)> validator;
|
||||
Fn<void(object_ptr<BoxContent>)> showBox;
|
||||
QString defaultPhone;
|
||||
QString defaultCountry;
|
||||
int maxLength = 0;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
class Field final {
|
||||
|
@ -49,20 +94,40 @@ public:
|
|||
[[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;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#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 "styles/style_payments.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
@ -267,7 +268,8 @@ void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
|
|||
push(_information.shippingAddress.address2);
|
||||
push(_information.shippingAddress.city);
|
||||
push(_information.shippingAddress.state);
|
||||
push(_information.shippingAddress.countryIso2);
|
||||
push(Data::CountryNameByISO2(
|
||||
_information.shippingAddress.countryIso2));
|
||||
push(_information.shippingAddress.postcode);
|
||||
add(
|
||||
tr::lng_payments_shipping_address(),
|
||||
|
|
|
@ -87,6 +87,7 @@ struct Address {
|
|||
};
|
||||
|
||||
struct RequestedInformation {
|
||||
QString defaultPhone;
|
||||
QString defaultCountry;
|
||||
|
||||
QString name;
|
||||
|
@ -144,7 +145,7 @@ struct PaymentMethodDetails {
|
|||
|
||||
enum class CardField {
|
||||
Number,
|
||||
CVC,
|
||||
Cvc,
|
||||
ExpireDate,
|
||||
Name,
|
||||
AddressCountry,
|
||||
|
|
|
@ -28,7 +28,7 @@ QString LastValidISO;
|
|||
|
||||
class CountrySelectBox::Inner : public TWidget {
|
||||
public:
|
||||
Inner(QWidget *parent, Type type);
|
||||
Inner(QWidget *parent, const QString &iso, Type type);
|
||||
~Inner();
|
||||
|
||||
void updateFilter(QString filter = QString());
|
||||
|
@ -93,10 +93,7 @@ CountrySelectBox::CountrySelectBox(QWidget*)
|
|||
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
|
||||
: _type(type)
|
||||
, _select(this, st::defaultMultiSelect, tr::lng_country_ph())
|
||||
, _ownedInner(this, type) {
|
||||
if (Data::CountriesByISO2().contains(iso)) {
|
||||
LastValidISO = iso;
|
||||
}
|
||||
, _ownedInner(this, iso, type) {
|
||||
}
|
||||
|
||||
rpl::producer<QString> CountrySelectBox::countryChosen() const {
|
||||
|
@ -169,7 +166,10 @@ void CountrySelectBox::setInnerFocus() {
|
|||
_select->setInnerFocus();
|
||||
}
|
||||
|
||||
CountrySelectBox::Inner::Inner(QWidget *parent, Type type)
|
||||
CountrySelectBox::Inner::Inner(
|
||||
QWidget *parent,
|
||||
const QString &iso,
|
||||
Type type)
|
||||
: TWidget(parent)
|
||||
, _type(type)
|
||||
, _rowHeight(st::countryRowHeight) {
|
||||
|
@ -177,6 +177,10 @@ CountrySelectBox::Inner::Inner(QWidget *parent, Type type)
|
|||
|
||||
const auto &byISO2 = Data::CountriesByISO2();
|
||||
|
||||
if (byISO2.contains(iso)) {
|
||||
LastValidISO = iso;
|
||||
}
|
||||
|
||||
_list.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 "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(
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
|
|
@ -90,6 +90,8 @@ private:
|
|||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString ExtractPhonePrefix(const QString &phone);
|
||||
|
||||
class PhoneInput : public MaskedInputField {
|
||||
public:
|
||||
PhoneInput(
|
||||
|
|
|
@ -21,6 +21,8 @@ PRIVATE
|
|||
stripe/stripe_card.h
|
||||
stripe/stripe_card_params.cpp
|
||||
stripe/stripe_card_params.h
|
||||
stripe/stripe_card_validator.cpp
|
||||
stripe/stripe_card_validator.h
|
||||
stripe/stripe_decode.cpp
|
||||
stripe/stripe_decode.h
|
||||
stripe/stripe_error.cpp
|
||||
|
|
|
@ -146,7 +146,9 @@ PRIVATE
|
|||
ui/cached_round_corners.h
|
||||
ui/grouped_layout.cpp
|
||||
ui/grouped_layout.h
|
||||
|
||||
ui/special_fields.cpp
|
||||
ui/special_fields.h
|
||||
|
||||
ui/ui_pch.h
|
||||
)
|
||||
|
||||
|
@ -163,4 +165,5 @@ PUBLIC
|
|||
PRIVATE
|
||||
desktop-app::lib_ffmpeg
|
||||
desktop-app::lib_webview
|
||||
desktop-app::lib_stripe
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue