Add native card input support through smartglocal.

This commit is contained in:
John Preston 2021-04-07 16:50:55 +04:00
parent 79f7aa703a
commit 61d0cc38b0
16 changed files with 732 additions and 24 deletions

View File

@ -272,9 +272,25 @@ void CheckoutProcess::handleError(const Error &error) {
} else if (id == u"ProcessingError"_q) {
showToast({ "Sorry, a processing error occurred." });
} else {
showToast({ "Error: " + id });
showToast({ "Stripe Error: " + id });
}
} break;
case Error::Type::SmartGlocal: {
//using Field = Ui::CardField;
//if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) {
// showCardError(Field::Number);
//} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
// showCardError(Field::Cvc);
//} else if (id == u"InvalidExpiryMonth"_q
// || id == u"InvalidExpiryYear"_q
// || id == u"ExpiredCard"_q) {
// showCardError(Field::ExpireDate);
//} else if (id == u"CardDeclined"_q) {
// showToast({ tr::lng_payments_card_declined(tr::now) });
//} else {
showToast({ "SmartGlocal Error: " + id });
//}
} break;
case Error::Type::TmpPassword:
if (const auto box = _enterPasswordBox.data()) {
if (!box->handleCustomCheckError(id)) {

View File

@ -21,6 +21,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
#include "stripe/stripe_card_validator.h"
#include "smartglocal/smartglocal_api_client.h"
#include "smartglocal/smartglocal_error.h"
#include "smartglocal/smartglocal_token.h"
#include "storage/storage_account.h"
#include "ui/image/image.h"
#include "apiwrap.h"
@ -96,6 +99,13 @@ constexpr auto kPasswordPeriod = 15 * TimeId(60);
+ card.last4();
}
[[nodiscard]] QString CardTitle(const SmartGlocal::Card &card) {
// Like server stores saved_credentials title.
return card.type().toLower()
+ " *"
+ SmartGlocal::Last4(card);
}
} // namespace
Form::Form(not_null<PeerData*> peer, MsgId itemId, bool receipt)
@ -427,33 +437,39 @@ void Form::fillPaymentMethodInformation() {
_paymentMethod.native = NativePaymentMethod();
_paymentMethod.ui.native = Ui::NativeMethodDetails();
_paymentMethod.ui.url = _details.url;
if (_details.nativeProvider == "stripe") {
fillStripeNativeMethod();
if (!_details.nativeProvider.isEmpty()) {
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(
_details.nativeParamsJson,
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payment Error: Could not decode native_params, error %1: %2"
).arg(error.error
).arg(error.errorString()));
} else if (!document.isObject()) {
LOG(("Payment Error: Not an object in native_params."));
} else {
const auto object = document.object();
if (_details.nativeProvider == "stripe") {
fillStripeNativeMethod(object);
} else if (_details.nativeProvider == "smartglocal") {
fillSmartGlocalNativeMethod(object);
} else {
LOG(("Payment Error: Unknown native provider '%1'."
).arg(_details.nativeProvider));
}
}
}
refreshPaymentMethodDetails();
}
void Form::fillStripeNativeMethod() {
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(
_details.nativeParamsJson,
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payment Error: Could not decode native_params, error %1: %2"
).arg(error.error
).arg(error.errorString()));
return;
} else if (!document.isObject()) {
LOG(("Payment Error: Not an object in native_params."));
return;
}
const auto object = document.object();
void Form::fillStripeNativeMethod(QJsonObject object) {
const auto value = [&](QStringView key) {
return object.value(key);
};
const auto key = value(u"publishable_key").toString();
if (key.isEmpty()) {
LOG(("Payment Error: No publishable_key in native_params."));
LOG(("Payment Error: No publishable_key in stripe native_params."));
return;
}
_paymentMethod.native = NativePaymentMethod{
@ -469,6 +485,29 @@ void Form::fillStripeNativeMethod() {
};
}
void Form::fillSmartGlocalNativeMethod(QJsonObject object) {
const auto value = [&](QStringView key) {
return object.value(key);
};
const auto key = value(u"public_token").toString();
if (key.isEmpty()) {
LOG(("Payment Error: "
"No public_token in smartglocal native_params."));
return;
}
_paymentMethod.native = NativePaymentMethod{
.data = SmartGlocalPaymentMethod{
.publicToken = key,
},
};
_paymentMethod.ui.native = Ui::NativeMethodDetails{
.supported = true,
.needCountry = false,
.needZip = false,
.needCardholderName = false,
};
}
void Form::submit() {
Expects(_paymentMethod.newCredentials
|| _paymentMethod.savedCredentials);
@ -602,6 +641,7 @@ bool Form::hasChanges() const {
: _information;
return (information != _savedInformation)
|| (_stripe != nullptr)
|| (_smartglocal != nullptr)
|| !_paymentMethod.newCredentials.empty();
}
@ -655,7 +695,10 @@ void Form::validateCard(
return;
}
const auto &native = _paymentMethod.native.data;
if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
if (const auto smartglocal = std::get_if<SmartGlocalPaymentMethod>(
&native)) {
validateCard(*smartglocal, details, saveInformation);
} else if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
validateCard(*stripe, details, saveInformation);
} else {
Unexpected("Native payment provider in Form::validateCard.");
@ -755,6 +798,57 @@ void Form::validateCard(
}));
}
void Form::validateCard(
const SmartGlocalPaymentMethod &method,
const Ui::UncheckedCardDetails &details,
bool saveInformation) {
Expects(!method.publicToken.isEmpty());
if (_smartglocal) {
return;
}
auto configuration = SmartGlocal::PaymentConfiguration{
.publicToken = method.publicToken,
.isTest = _invoice.isTest,
};
_smartglocal = std::make_unique<SmartGlocal::APIClient>(
std::move(configuration));
auto card = Stripe::CardParams{
.number = details.number,
.expMonth = details.expireMonth,
.expYear = details.expireYear,
.cvc = details.cvc,
.name = details.cardholderName,
.addressZip = details.addressZip,
.addressCountry = details.addressCountry,
};
_smartglocal->createTokenWithCard(std::move(card), crl::guard(this, [=](
SmartGlocal::Token token,
SmartGlocal::Error error) {
_smartglocal = nullptr;
if (error) {
LOG(("SmartGlocal Error %1: %2 (%3)"
).arg(int(error.code())
).arg(error.description()
).arg(error.message()));
_updates.fire(Error{
Error::Type::SmartGlocal,
error.description(),
});
} else {
setPaymentCredentials({
.title = CardTitle(token.card()),
.data = QJsonDocument(QJsonObject{
{ "token", token.tokenId() },
{ "type", "card" },
}).toJson(QJsonDocument::Compact),
.saveOnServer = saveInformation,
});
}
}));
}
void Form::setPaymentCredentials(const NewCredentials &credentials) {
Expects(!credentials.empty());

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/sender.h"
class Image;
class QJsonObject;
namespace Core {
struct CloudPasswordResult;
@ -21,6 +22,10 @@ namespace Stripe {
class APIClient;
} // namespace Stripe
namespace SmartGlocal {
class APIClient;
} // namespace SmartGlocal
namespace Main {
class Session;
} // namespace Main
@ -86,10 +91,15 @@ struct StripePaymentMethod {
QString publishableKey;
};
struct SmartGlocalPaymentMethod {
QString publicToken;
};
struct NativePaymentMethod {
std::variant<
v::null_t,
StripePaymentMethod> data;
StripePaymentMethod,
SmartGlocalPaymentMethod> data;
[[nodiscard]] bool valid() const {
return !v::is_null(data);
@ -131,6 +141,7 @@ struct Error {
Form,
Validate,
Stripe,
SmartGlocal,
TmpPassword,
Send,
};
@ -221,7 +232,8 @@ private:
const MTPDpaymentSavedCredentialsCard &data);
void processShippingOptions(const QVector<MTPShippingOption> &data);
void fillPaymentMethodInformation();
void fillStripeNativeMethod();
void fillStripeNativeMethod(QJsonObject object);
void fillSmartGlocalNativeMethod(QJsonObject object);
void refreshPaymentMethodDetails();
[[nodiscard]] QString defaultPhone() const;
[[nodiscard]] QString defaultCountry() const;
@ -230,6 +242,10 @@ private:
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details,
bool saveInformation);
void validateCard(
const SmartGlocalPaymentMethod &method,
const Ui::UncheckedCardDetails &details,
bool saveInformation);
bool validateInformationLocal(
const Ui::RequestedInformation &information) const;
@ -259,6 +275,7 @@ private:
mtpRequestId _passwordRequestId = 0;
std::unique_ptr<Stripe::APIClient> _stripe;
std::unique_ptr<SmartGlocal::APIClient> _smartglocal;
Ui::ShippingOptions _shippingOptions;
QString _requestedInformationId;

View File

@ -0,0 +1,169 @@
/*
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 "smartglocal/smartglocal_api_client.h"
#include "smartglocal/smartglocal_error.h"
#include "smartglocal/smartglocal_token.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
#include <windows.h>
#include <debugapi.h>
namespace SmartGlocal {
namespace {
[[nodiscard]] QString APIURLBase(bool isTest) {
return isTest
? "tgb-playground.smart-glocal.com/cds/v1"
: "tgb.smart-glocal.com/cds/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokenize/card";
}
[[nodiscard]] QByteArray ToJson(const Stripe::CardParams &card) {
const auto zero = QChar('0');
const auto month = QString("%1").arg(card.expMonth, 2, 10, zero);
const auto year = QString("%1").arg(card.expYear % 100, 2, 10, zero);
return QJsonDocument(QJsonObject{
{ "card", QJsonObject{
{ "number", card.number },
{ "expiration_month", month },
{ "expiration_year", year },
{ "security_code", card.cvc },
} },
}).toJson(QJsonDocument::Compact);
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl("https://" + APIURLBase(configuration.isTest))
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-PUBLIC-TOKEN", _configuration.publicToken },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
Stripe::CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(ToJson(card), std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl + '/' + TokenEndpoint());
auto request = QNetworkRequest(url);
request.setHeader(
QNetworkRequest::ContentTypeHeader,
"application/json");
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().value("data").toObject());
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 SmartGlocal

View File

@ -0,0 +1,48 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_card_params.h"
#include "smartglocal/smartglocal_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
namespace SmartGlocal {
struct PaymentConfiguration {
QString publicToken;
bool isTest = false;
};
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
Stripe::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 SmartGlocal

View File

@ -0,0 +1,17 @@
/*
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 SmartGlocal {
class Token;
class Error;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace SmartGlocal

View File

@ -0,0 +1,60 @@
/*
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 "smartglocal/smartglocal_card.h"
namespace SmartGlocal {
Card::Card(
QString type,
QString network,
QString maskedNumber)
: _type(type)
, _network(network)
, _maskedNumber(maskedNumber) {
}
Card Card::Empty() {
return Card(QString(), QString(), QString());
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto type = string(u"card_type");
const auto network = string(u"card_network");
const auto maskedNumber = string(u"masked_card_number");
if (type.isEmpty() || maskedNumber.isEmpty()) {
return Card::Empty();
}
return Card(type, network, maskedNumber);
}
QString Card::type() const {
return _type;
}
QString Card::network() const {
return _network;
}
QString Card::maskedNumber() const {
return _maskedNumber;
}
bool Card::empty() const {
return _type.isEmpty() || _maskedNumber.isEmpty();
}
QString Last4(const Card &card) {
const auto masked = card.maskedNumber();
const auto m = QRegularExpression("[^\\d]\\d*(\\d{4})$").match(masked);
return m.hasMatch() ? m.captured(1) : QString();
}
} // namespace SmartGlocal

View File

@ -0,0 +1,51 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QString>
class QJsonObject;
namespace SmartGlocal {
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 type() const;
[[nodiscard]] QString network() const;
[[nodiscard]] QString maskedNumber() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Card(
QString type,
QString network,
QString maskedNumber);
QString _type;
QString _network;
QString _maskedNumber;
};
[[nodiscard]] QString Last4(const Card &card);
} // namespace SmartGlocal

View File

@ -0,0 +1,69 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "smartglocal/smartglocal_error.h"
namespace SmartGlocal {
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) {
if (object.value("status").toString() == "ok") {
return Error::None();
}
const auto entry = object.value("error");
if (!entry.isObject()) {
return {
Code::Unknown,
"GenericError",
"Could not read the error response "
"that was returned from SmartGlocal."
};
}
const auto error = entry.toObject();
const auto string = [&](QStringView key) {
return error.value(key).toString();
};
const auto code = string(u"code");
const auto description = string(u"description");
// There should always be a message and type for the error
if (code.isEmpty() || description.isEmpty()) {
return {
Code::Unknown,
"GenericError",
"Could not interpret the error response "
"that was returned from SmartGlocal."
};
}
return { Code::Unknown, code, description };
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace SmartGlocal

View File

@ -0,0 +1,59 @@
/*
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 SmartGlocal {
class Error {
public:
enum class Code {
None = 0, // Non-SmartGlocal errors.
JsonParse = -1,
JsonFormat = -2,
Network = -3,
Unknown = 8,
};
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 SmartGlocal

View File

@ -0,0 +1,46 @@
/*
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 "smartglocal/smartglocal_token.h"
namespace SmartGlocal {
QString Token::tokenId() const {
return _tokenId;
}
Card Token::card() const {
return _card;
}
Token Token::Empty() {
return Token(QString());
}
Token Token::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto tokenId = object.value("token").toString();
if (tokenId.isEmpty()) {
return Token::Empty();
}
auto result = Token(tokenId);
const auto card = object.value("info");
if (card.isObject()) {
result._card = Card::DecodedObjectFromAPIResponse(card.toObject());
}
return result;
}
bool Token::empty() const {
return _tokenId.isEmpty();
}
Token::Token(QString tokenId)
: _tokenId(std::move(tokenId)) {
}
} // namespace SmartGlocal

View File

@ -0,0 +1,47 @@
/*
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 "smartglocal/smartglocal_card.h"
#include <QtCore/QDateTime>
class QJsonObject;
namespace SmartGlocal {
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:
explicit Token(QString tokenId);
QString _tokenId;
Card _card = Card::Empty();
};
} // namespace SmartGlocal

View File

@ -51,7 +51,7 @@ APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl("https://" + APIURLBase())
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-Stripe-User-Agent", StripeUserAgentDetails() },
{ "X-Stripe-User-Agent", StripeUserAgentDetails() },
{ "Stripe-Version", StripeAPIVersion() },
{ "Authorization", "Bearer " + _configuration.publishableKey },
};

View File

@ -225,6 +225,11 @@ struct SimpleFieldState {
).toDouble();
return QString::number(
int64(std::round(real * std::pow(10., rule.exponent))));
} else if (config.type == FieldType::CardNumber
|| config.type == FieldType::CardCVC) {
return QString(parsed).replace(
QRegularExpression("[^0-9\\.]"),
QString());
}
return parsed;
}

View File

@ -33,6 +33,16 @@ PRIVATE
stripe/stripe_payment_configuration.h
stripe/stripe_token.cpp
stripe/stripe_token.h
smartglocal/smartglocal_api_client.cpp
smartglocal/smartglocal_api_client.h
smartglocal/smartglocal_callbacks.h
smartglocal/smartglocal_card.cpp
smartglocal/smartglocal_card.h
smartglocal/smartglocal_error.cpp
smartglocal/smartglocal_error.h
smartglocal/smartglocal_token.cpp
smartglocal/smartglocal_token.h
stripe/stripe_pch.h
)

@ -1 +1 @@
Subproject commit 49887261a55665f6e195049bcc22b6495a44cc36
Subproject commit c1548226d49db23f68bbf35f34cc820171aed65c