init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Stripe {
enum class BillingAddressFields {
None,
Zip,
Full,
};
} // namespace Stripe

View File

@@ -0,0 +1,167 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_api_client.h"
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
#include "stripe/stripe_form_encoder.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
namespace Stripe {
namespace {
[[nodiscard]] QString APIURLBase() {
return "api.stripe.com/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokens";
}
[[nodiscard]] QString StripeAPIVersion() {
return "2015-10-12";
}
[[nodiscard]] QString SDKVersion() {
return "9.1.0";
}
[[nodiscard]] QString StripeUserAgentDetails() {
const auto details = QJsonObject{
{ "lang", "objective-c" },
{ "bindings_version", SDKVersion() },
};
return QString::fromUtf8(
QJsonDocument(details).toJson(QJsonDocument::Compact));
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl("https://" + APIURLBase())
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-Stripe-User-Agent", StripeUserAgentDetails() },
{ "Stripe-Version", StripeAPIVersion() },
{ "Authorization", "Bearer " + _configuration.publishableKey },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(
FormEncoder::formEncodedDataForObject(MakeEncodable(card)),
std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl + '/' + TokenEndpoint());
auto request = QNetworkRequest(url);
for (const auto &[name, value] : _additionalHttpHeaders) {
request.setRawHeader(name.toUtf8(), value.toUtf8());
}
destroyReplyDelayed(std::move(_reply));
_reply.reset(_manager.post(request, data));
const auto finish = [=](Token token, Error error) {
crl::on_main([
completion,
token = std::move(token),
error = std::move(error)
] {
completion(std::move(token), std::move(error));
});
};
const auto finishWithError = [=](Error error) {
finish(Token::Empty(), std::move(error));
};
const auto finishWithToken = [=](Token token) {
finish(std::move(token), Error::None());
};
QObject::connect(_reply.get(), &QNetworkReply::finished, [=] {
const auto replyError = int(_reply->error());
const auto replyErrorString = _reply->errorString();
const auto bytes = _reply->readAll();
destroyReplyDelayed(std::move(_reply));
auto parseError = QJsonParseError();
const auto document = QJsonDocument::fromJson(bytes, &parseError);
if (!bytes.isEmpty()) {
if (parseError.error != QJsonParseError::NoError) {
const auto code = int(parseError.error);
finishWithError({
Error::Code::JsonParse,
QString("InvalidJson%1").arg(code),
parseError.errorString(),
});
return;
} else if (!document.isObject()) {
finishWithError({
Error::Code::JsonFormat,
"InvalidJsonRoot",
"Not an object in JSON reply.",
});
return;
}
const auto object = document.object();
if (auto error = Error::DecodedObjectFromResponse(object)) {
finishWithError(std::move(error));
return;
}
}
if (replyError != QNetworkReply::NoError) {
finishWithError({
Error::Code::Network,
QString("RequestError%1").arg(replyError),
replyErrorString,
});
return;
}
auto token = Token::DecodedObjectFromAPIResponse(document.object());
if (!token) {
finishWithError({
Error::Code::JsonFormat,
"InvalidTokenJson",
"Could not parse token.",
});
}
finishWithToken(std::move(token));
});
}
void APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {
if (!reply) {
return;
}
const auto raw = reply.get();
_old.push_back(std::move(reply));
QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);
raw->deleteLater();
QObject::connect(raw, &QObject::destroyed, [=] {
for (auto i = begin(_old); i != end(_old); ++i) {
if (i->get() == raw) {
i->release();
_old.erase(i);
break;
}
}
});
}
} // namespace Stripe

View File

@@ -0,0 +1,45 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_payment_configuration.h"
#include "stripe/stripe_card_params.h"
#include "stripe/stripe_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
#include <memory>
namespace Stripe {
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
CardParams card,
TokenCompletionCallback completion);
void createTokenWithData(
QByteArray data,
TokenCompletionCallback completion);
private:
void destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace Stripe

View File

@@ -0,0 +1,19 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <functional>
namespace Stripe {
class Error;
class Token;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace Stripe

View File

@@ -0,0 +1,188 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_card.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
namespace {
CardBrand BrandFromString(const QString &brand) {
if (brand == "visa") {
return CardBrand::Visa;
} else if (brand == "american express") {
return CardBrand::Amex;
} else if (brand == "mastercard") {
return CardBrand::MasterCard;
} else if (brand == "discover") {
return CardBrand::Discover;
} else if (brand == "jcb") {
return CardBrand::JCB;
} else if (brand == "diners club") {
return CardBrand::DinersClub;
} else {
return CardBrand::Unknown;
}
}
CardFundingType FundingFromString(const QString &funding) {
if (funding == "credit") {
return CardFundingType::Credit;
} else if (funding == "debit") {
return CardFundingType::Debit;
} else if (funding == "prepaid") {
return CardFundingType::Prepaid;
} else {
return CardFundingType::Other;
}
}
} // namespace
Card::Card(
QString id,
QString last4,
CardBrand brand,
quint32 expMonth,
quint32 expYear)
: _cardId(id)
, _last4(last4)
, _brand(brand)
, _expMonth(expMonth)
, _expYear(expYear) {
}
Card Card::Empty() {
return Card(QString(), QString(), CardBrand::Unknown, 0, 0);
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
if (!ContainsFields(object, {
u"id",
u"last4",
u"brand",
u"exp_month",
u"exp_year"
})) {
return Card::Empty();
}
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto cardId = string(u"id");
const auto last4 = string(u"last4");
const auto brand = BrandFromString(string(u"brand").toLower());
const auto expMonth = object.value("exp_month").toInt();
const auto expYear = object.value("exp_year").toInt();
auto result = Card(cardId, last4, brand, expMonth, expYear);
result._name = string(u"name");
result._dynamicLast4 = string(u"dynamic_last4");
result._funding = FundingFromString(string(u"funding").toLower());
result._fingerprint = string(u"fingerprint");
result._country = string(u"country");
result._currency = string(u"currency");
result._addressLine1 = string(u"address_line1");
result._addressLine2 = string(u"address_line2");
result._addressCity = string(u"address_city");
result._addressState = string(u"address_state");
result._addressZip = string(u"address_zip");
result._addressCountry = string(u"address_country");
// TODO incomplete, not used.
//result._allResponseFields = object;
return result;
}
QString Card::cardId() const {
return _cardId;
}
QString Card::name() const {
return _name;
}
QString Card::last4() const {
return _last4;
}
QString Card::dynamicLast4() const {
return _dynamicLast4;
}
CardBrand Card::brand() const {
return _brand;
}
CardFundingType Card::funding() const {
return _funding;
}
QString Card::fingerprint() const {
return _fingerprint;
}
QString Card::country() const {
return _country;
}
QString Card::currency() const {
return _currency;
}
quint32 Card::expMonth() const {
return _expMonth;
}
quint32 Card::expYear() const {
return _expYear;
}
QString Card::addressLine1() const {
return _addressLine1;
}
QString Card::addressLine2() const {
return _addressLine2;
}
QString Card::addressCity() const {
return _addressCity;
}
QString Card::addressState() const {
return _addressState;
}
QString Card::addressZip() const {
return _addressZip;
}
QString Card::addressCountry() const {
return _addressCountry;
}
bool Card::empty() const {
return _cardId.isEmpty();
}
QString CardBrandToString(CardBrand brand) {
switch (brand) {
case CardBrand::Amex: return "American Express";
case CardBrand::DinersClub: return "Diners Club";
case CardBrand::Discover: return "Discover";
case CardBrand::JCB: return "JCB";
case CardBrand::MasterCard: return "MasterCard";
case CardBrand::Unknown: return "Unknown";
case CardBrand::Visa: return "Visa";
}
std::abort();
}
} // namespace Stripe

View File

@@ -0,0 +1,99 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QString>
class QJsonObject;
namespace Stripe {
enum class CardBrand {
Visa,
Amex,
MasterCard,
Discover,
JCB,
DinersClub,
UnionPay,
Unknown,
};
enum class CardFundingType {
Debit,
Credit,
Prepaid,
Other,
};
class Card final {
public:
Card(const Card &other) = default;
Card &operator=(const Card &other) = default;
Card(Card &&other) = default;
Card &operator=(Card &&other) = default;
~Card() = default;
[[nodiscard]] static Card Empty();
[[nodiscard]] static Card DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] QString cardId() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString last4() const;
[[nodiscard]] QString dynamicLast4() const;
[[nodiscard]] CardBrand brand() const;
[[nodiscard]] CardFundingType funding() const;
[[nodiscard]] QString fingerprint() const;
[[nodiscard]] QString country() const;
[[nodiscard]] QString currency() const;
[[nodiscard]] quint32 expMonth() const;
[[nodiscard]] quint32 expYear() const;
[[nodiscard]] QString addressLine1() const;
[[nodiscard]] QString addressLine2() const;
[[nodiscard]] QString addressCity() const;
[[nodiscard]] QString addressState() const;
[[nodiscard]] QString addressZip() const;
[[nodiscard]] QString addressCountry() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Card(
QString id,
QString last4,
CardBrand brand,
quint32 expMonth,
quint32 expYear);
QString _cardId;
QString _name;
QString _last4;
QString _dynamicLast4;
CardBrand _brand = CardBrand::Unknown;
CardFundingType _funding = CardFundingType::Other;
QString _fingerprint;
QString _country;
QString _currency;
quint32 _expMonth = 0;
quint32 _expYear = 0;
QString _addressLine1;
QString _addressLine2;
QString _addressCity;
QString _addressState;
QString _addressZip;
QString _addressCountry;
};
[[nodiscard]] QString CardBrandToString(CardBrand brand);
} // namespace Stripe

View File

@@ -0,0 +1,33 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_card_params.h"
namespace Stripe {
QString CardParams::rootObjectName() {
return "card";
}
std::map<QString, QString> CardParams::formFieldValues() const {
return {
{ "number", number },
{ "cvc", cvc },
{ "name", name },
{ "address_line1", addressLine1 },
{ "address_line2", addressLine2 },
{ "address_city", addressCity },
{ "address_state", addressState },
{ "address_zip", addressZip },
{ "address_country", addressCountry },
{ "exp_month", QString::number(expMonth) },
{ "exp_year", QString::number(expYear) },
{ "currency", currency },
};
}
} // namespace Stripe

View File

@@ -0,0 +1,32 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_form_encodable.h"
namespace Stripe {
struct CardParams {
QString number;
quint32 expMonth = 0;
quint32 expYear = 0;
QString cvc;
QString name;
QString addressLine1;
QString addressLine2;
QString addressCity;
QString addressState;
QString addressZip;
QString addressCountry;
QString currency;
[[nodiscard]] static QString rootObjectName();
[[nodiscard]] std::map<QString, QString> formFieldValues() const;
};
} // namespace Stripe

View File

@@ -0,0 +1,304 @@
/*
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 "base/qt/qt_string_view.h"
#include <QtCore/QDate>
#include <QtCore/QRegularExpression>
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 (base::StringViewMid(sanitized, 0, minWithLow).toInt()
< base::StringViewMid(range.low, 0, minWithLow).toInt()) {
return false;
}
const auto minWithHigh = std::min(sanitized.size(), range.high.size());
if (base::StringViewMid(sanitized, 0, minWithHigh).toInt()
> base::StringViewMid(range.high, 0, minWithHigh).toInt()) {
return false;
}
return true;
}
[[nodiscard]] bool IsNumeric(const QString &value) {
static const auto RegExp = QRegularExpression("^[0-9]*$");
return RegExp.match(value).hasMatch();
}
[[nodiscard]] QString RemoveWhitespaces(QString value) {
static const auto RegExp = QRegularExpression("\\s");
return value.replace(RegExp, 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;
static const auto &all = AllRanges();
static const auto compare = [](const BinRange &a, const BinRange &b) {
return a.length < b.length;
};
static const auto kMinLength = std::min_element(
begin(all),
end(all),
compare)->length;
static const auto kMaxLength = std::max_element(
begin(all),
end(all),
compare)->length;
if (sanitized.size() > kMaxLength) {
return { .state = ValidationState::Invalid, .brand = brand };
} else if (sanitized.size() < kMinLength) {
return { .state = ValidationState::Incomplete, .brand = brand };
} else if (!IsValidLuhn(sanitized)) {
return { .state = ValidationState::Invalid, .brand = brand };
} else if (sanitized.size() < kMaxLength) {
return { .state = ValidationState::Valid, .brand = brand };
}
return {
.state = ValidationState::Valid,
.brand = brand,
.finished = true,
};
}
ExpireDateValidationResult ValidateExpireDate(
const QString &date,
const std::optional<QDate> &overrideExpireDateThreshold) {
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 = base::StringViewMid(normalized, 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 + base::StringViewMid(normalized, 2).toInt();
const auto thresholdDate = overrideExpireDateThreshold.value_or(
QDate::currentDate());
const auto thresholdMonth = thresholdDate.month();
const auto thresholdYear = thresholdDate.year();
if (year < thresholdYear) {
return { ValidationState::Invalid };
} else if (year == thresholdYear && month < thresholdMonth) {
return { ValidationState::Invalid };
}
return { ValidationState::Valid, true };
}
ValidationState ValidateParsedExpireDate(
quint32 month,
quint32 year,
const std::optional<QDate> &overrideExpireDateThreshold) {
if ((year / 100) != 20) {
return ValidationState::Invalid;
}
const auto date = QString("%1%2"
).arg(month, 2, 10, QChar('0')
).arg(year % 100, 2, 10, QChar('0'));
return ValidateExpireDate(date, overrideExpireDateThreshold).state;
}
CvcValidationResult ValidateCvc(
const QString &number,
const QString &cvc) {
if (!IsNumeric(cvc)) {
return { ValidationState::Invalid };
} else if (cvc.size() < kMinCvcLength) {
return { ValidationState::Incomplete };
}
const auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number));
if (cvc.size() > maxLength) {
return { ValidationState::Invalid };
}
return { ValidationState::Valid, (cvc.size() == maxLength) };
}
std::vector<int> CardNumberFormat(const QString &number) {
static const auto kDefault = std::vector{ 4, 4, 4, 4 };
const auto sanitized = RemoveWhitespaces(number);
if (!IsNumeric(sanitized)) {
return kDefault;
}
const auto range = MostSpecificBinRangeForNumber(sanitized);
if (range.brand == CardBrand::DinersClub && range.length == 14) {
return { 4, 6, 4 };
} else if (range.brand == CardBrand::Amex) {
return { 4, 6, 5 };
}
return kDefault;
}
} // namespace Stripe

View File

@@ -0,0 +1,57 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_card.h"
#include <vector>
#include <optional>
class QDate;
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,
const std::optional<QDate> &overrideExpireDateThreshold);
[[nodiscard]] ValidationState ValidateParsedExpireDate(
quint32 month,
quint32 year,
const std::optional<QDate> &overrideExpireDateThreshold);
struct CvcValidationResult {
ValidationState state = ValidationState::Invalid;
bool finished = false;
};
[[nodiscard]] CvcValidationResult ValidateCvc(
const QString &number,
const QString &cvc);
[[nodiscard]] std::vector<int> CardNumberFormat(const QString &number);
} // namespace Stripe

View File

@@ -0,0 +1,23 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_decode.h"
namespace Stripe {
[[nodiscard]] bool ContainsFields(
const QJsonObject &object,
std::vector<QStringView> keys) {
for (const auto &key : keys) {
if (object.value(key).isUndefined()) {
return false;
}
}
return true;
}
} // namespace Stripe

View File

@@ -0,0 +1,19 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QJsonObject>
#include <vector>
namespace Stripe {
[[nodiscard]] bool ContainsFields(
const QJsonObject &object,
std::vector<QStringView> keys);
} // namespace Stripe

View File

@@ -0,0 +1,107 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_error.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
Error::Code Error::code() const {
return _code;
}
QString Error::description() const {
return _description;
}
QString Error::message() const {
return _message;
}
QString Error::parameter() const {
return _parameter;
}
Error Error::None() {
return Error(Code::None, {}, {}, {});
}
Error Error::DecodedObjectFromResponse(QJsonObject object) {
const auto entry = object.value("error");
if (!entry.isObject()) {
return Error::None();
}
const auto error = entry.toObject();
const auto string = [&](QStringView key) {
return error.value(key).toString();
};
const auto type = string(u"type");
const auto message = string(u"message");
const auto parameterSnakeCase = string(u"param");
// There should always be a message and type for the error
if (message.isEmpty() || type.isEmpty()) {
return {
Code::API,
"GenericError",
"Could not interpret the error response "
"that was returned from Stripe."
};
}
auto parameterWords = parameterSnakeCase.isEmpty()
? QStringList()
: parameterSnakeCase.split('_', Qt::SkipEmptyParts);
auto first = true;
for (auto &word : parameterWords) {
if (first) {
first = false;
} else {
word = word[0].toUpper() + word.mid(1);
}
}
const auto parameter = parameterWords.join(QString());
if (type == "api_error") {
return { Code::API, "GenericError", message, parameter };
} else if (type == "invalid_request_error") {
return { Code::InvalidRequest, "GenericError", message, parameter };
} else if (type != "card_error") {
return { Code::Unknown, type, message, parameter };
}
const auto code = string(u"code");
const auto cardError = [&](const QString &description) {
return Error{ Code::Card, description, message, parameter };
};
if (code == "incorrect_number") {
return cardError("IncorrectNumber");
} else if (code == "invalid_number") {
return cardError("InvalidNumber");
} else if (code == "invalid_expiry_month") {
return cardError("InvalidExpiryMonth");
} else if (code == "invalid_expiry_year") {
return cardError("InvalidExpiryYear");
} else if (code == "invalid_cvc") {
return cardError("InvalidCVC");
} else if (code == "expired_card") {
return cardError("ExpiredCard");
} else if (code == "incorrect_cvc") {
return cardError("IncorrectCVC");
} else if (code == "card_declined") {
return cardError("CardDeclined");
} else if (code == "processing_error") {
return cardError("ProcessingError");
} else {
return cardError(code);
}
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace Stripe

View File

@@ -0,0 +1,66 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QString>
class QJsonObject;
namespace Stripe {
class Error {
public:
enum class Code {
None = 0, // Non-Stripe errors.
JsonParse = -1,
JsonFormat = -2,
Network = -3,
Unknown = 8,
Connection = 40, // Trouble connecting to Stripe.
InvalidRequest = 50, // Your request had invalid parameters.
API = 60, // General-purpose API error (should be rare).
Card = 70, // Something was wrong with the given card (most common).
Cancellation = 80, // The operation was cancelled.
CheckoutUnknown = 5000, // Checkout failed
CheckoutTooManyAttempts = 5001, // Too many incorrect code attempts
};
Error(
Code code,
const QString &description,
const QString &message,
const QString &parameter = QString())
: _code(code)
, _description(description)
, _message(message)
, _parameter(parameter) {
}
[[nodiscard]] Code code() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString parameter() const;
[[nodiscard]] static Error None();
[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Code _code = Code::None;
QString _description;
QString _message;
QString _parameter;
};
} // namespace Stripe

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QString>
#include <map>
namespace Stripe {
class FormEncodable {
public:
[[nodiscard]] virtual QString rootObjectName() = 0;
[[nodiscard]] virtual std::map<QString, QString> formFieldValues() = 0;
};
template <typename T>
struct MakeEncodable final : FormEncodable {
public:
MakeEncodable(const T &value) : _value(value) {
}
QString rootObjectName() override {
return _value.rootObjectName();
}
std::map<QString, QString> formFieldValues() override {
return _value.formFieldValues();
}
private:
const T &_value;
};
} // namespace Stripe

View File

@@ -0,0 +1,41 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_form_encoder.h"
#include <QStringList>
#include <QUrl>
#include <vector>
namespace Stripe {
QByteArray FormEncoder::formEncodedDataForObject(
FormEncodable &&object) {
const auto root = object.rootObjectName();
const auto values = object.formFieldValues();
auto result = QByteArray();
auto keys = std::vector<QString>();
for (const auto &[key, value] : values) {
if (!value.isEmpty()) {
keys.push_back(key);
}
}
std::sort(begin(keys), end(keys));
const auto encode = [](const QString &value) {
return QUrl::toPercentEncoding(value);
};
for (const auto &key : keys) {
const auto fullKey = root.isEmpty() ? key : (root + '[' + key + ']');
if (!result.isEmpty()) {
result += '&';
}
result += encode(fullKey) + '=' + encode(values.at(key));
}
return result;
}
} // namespace Stripe

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_form_encodable.h"
namespace Stripe {
class FormEncoder {
public:
[[nodiscard]] static QByteArray formEncodedDataForObject(
FormEncodable &&object);
};
} // namespace Stripe

View File

@@ -0,0 +1,29 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_address.h"
#include <QtCore/QString>
namespace Stripe {
struct PaymentConfiguration {
QString publishableKey;
// PaymentMethodType additionalPaymentMethods; // Apply Pay
// TODO incomplete, not used.
//BillingAddressFields requiredBillingAddressFields
// = BillingAddressFields::None;
QString companyName;
// QString appleMerchantIdentifier; // Apple Pay
// bool smsAutofillDisabled = true; // Mobile only
};
} // namespace Stripe

View File

@@ -0,0 +1,13 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include <QtCore/QString>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonDocument>

View File

@@ -0,0 +1,65 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "stripe/stripe_token.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
QString Token::tokenId() const {
return _tokenId;
}
bool Token::livemode() const {
return _livemode;
}
Card Token::card() const {
return _card;
}
Token Token::Empty() {
return Token(QString(), false, QDateTime());
}
Token Token::DecodedObjectFromAPIResponse(QJsonObject object) {
if (!ContainsFields(object, { u"id", u"livemode", u"created" })) {
return Token::Empty();
}
const auto tokenId = object.value("id").toString();
const auto livemode = object.value("livemode").toBool();
const auto created = QDateTime::fromSecsSinceEpoch(
object.value("created").toDouble());
auto result = Token(tokenId, livemode, created);
const auto card = object.value("card");
if (card.isObject()) {
result._card = Card::DecodedObjectFromAPIResponse(card.toObject());
}
// TODO incomplete, not used.
//const auto bankAccount = object.value("bank_account");
//if (bankAccount.isObject()) {
// result._bankAccount = bankAccount::DecodedObjectFromAPIResponse(
// bankAccount.toObject());
//}
//result._allResponseFields = object;
return result;
}
bool Token::empty() const {
return _tokenId.isEmpty();
}
Token::Token(QString tokenId, bool livemode, QDateTime created)
: _tokenId(std::move(tokenId))
, _livemode(livemode)
, _created(std::move(created)) {
}
} // namespace Stripe

View File

@@ -0,0 +1,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_card.h"
#include <QtCore/QDateTime>
class QJsonObject;
namespace Stripe {
class Token {
public:
Token(const Token &other) = default;
Token &operator=(const Token &other) = default;
Token(Token &&other) = default;
Token &operator=(Token &&other) = default;
~Token() = default;
[[nodiscard]] QString tokenId() const;
[[nodiscard]] bool livemode() const;
[[nodiscard]] Card card() const;
[[nodiscard]] static Token Empty();
[[nodiscard]] static Token DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Token(QString tokenId, bool livemode, QDateTime created);
QString _tokenId;
bool _livemode = false;
QDateTime _created;
Card _card = Card::Empty();
};
} // namespace Stripe