Support entering card details natively.

This commit is contained in:
John Preston 2021-03-25 19:27:30 +04:00
parent 5bc6e6533f
commit 5e4bc200c2
25 changed files with 859 additions and 123 deletions

View File

@ -1863,20 +1863,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_checkout_title" = "Checkout";
"lng_payments_total_label" = "Total";
"lng_payments_pay_amount" = "Pay {amount}";
//"lng_payments_payment_method" = "Payment Method"; // #TODO payments native
"lng_payments_payment_method" = "Payment Method";
"lng_payments_payment_method_ph" = "Enter your card details";
"lng_payments_shipping_address" = "Shipping Information";
"lng_payments_shipping_address_ph" = "Enter your shipping information";
"lng_payments_shipping_method" = "Shipping Method";
"lng_payments_shipping_method_ph" = "Choose your shipping method";
"lng_payments_info_name" = "Name";
"lng_payments_info_name_ph" = "Enter your name";
"lng_payments_info_email" = "Email";
"lng_payments_info_email_ph" = "Enter your email";
"lng_payments_info_phone" = "Phone";
"lng_payments_info_phone_ph" = "Enter your phone number";
"lng_payments_shipping_address_title" = "Shipping Address";
"lng_payments_save_shipping_about" = "You can save your shipping information for future use.";
//"lng_payments_payment_card" = "Payment Card"; // #TODO payments native
//"lng_payments_cardholder_title" = "Cardholder";
//"lng_payments_cardholder_about" = "Cardholder Name";
//"lng_payments_billing_address" = "Billing Address";
//"lng_payments_zip_code" = "Zip Code";
//"lng_payments_save_payment_about" = "You can save your payment information for future use.";
"lng_payments_payment_card" = "Payment Card";
"lng_payments_cardholder_title" = "Cardholder";
"lng_payments_cardholder_about" = "Cardholder Name";
"lng_payments_billing_address" = "Billing Address";
"lng_payments_zip_code" = "Zip Code";
"lng_payments_save_payment_about" = "You can save your payment information for future use.";
"lng_payments_save_information" = "Save Information";
"lng_call_status_incoming" = "is calling you...";

View File

@ -18,6 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
#include "apiwrap.h"
#include "stripe/stripe_api_client.h"
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
// #TODO payments errors
#include "mainwindow.h"
@ -54,6 +57,13 @@ base::flat_map<not_null<Main::Session*>, SessionProcesses> Processes;
return result;
}
[[nodiscard]] QString CardTitle(const Stripe::Card &card) {
// Like server stores saved_credentials title.
return Stripe::CardBrandToString(card.brand()).toLower()
+ " *"
+ card.last4();
}
} // namespace
void CheckoutProcess::Start(not_null<const HistoryItem*> item) {
@ -167,22 +177,23 @@ void CheckoutProcess::handleError(const Error &error) {
showForm();
return;
}
using Field = Ui::InformationField;
if (id == u"REQ_INFO_NAME_INVALID"_q) {
showEditError(Ui::EditField::Name);
showInformationError(Field::Name);
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
showEditError(Ui::EditField::Email);
showInformationError(Field::Email);
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
showEditError(Ui::EditField::Phone);
showInformationError(Field::Phone);
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
showEditError(Ui::EditField::ShippingStreet);
showInformationError(Field::ShippingStreet);
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
showEditError(Ui::EditField::ShippingCity);
showInformationError(Field::ShippingCity);
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
showEditError(Ui::EditField::ShippingState);
showInformationError(Field::ShippingState);
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
showEditError(Ui::EditField::ShippingCountry);
showInformationError(Field::ShippingCountry);
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
showEditError(Ui::EditField::ShippingPostcode);
showInformationError(Field::ShippingPostcode);
} else if (id == u"SHIPPING_BOT_TIMEOUT"_q) {
showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
@ -238,6 +249,7 @@ void CheckoutProcess::panelSubmit() {
|| _submitState == SubmitState::Finishing) {
return;
}
const auto &native = _form->nativePayment();
const auto &invoice = _form->invoice();
const auto &options = _form->shippingOptions();
if (!options.list.empty() && options.selectedId.isEmpty()) {
@ -252,14 +264,23 @@ void CheckoutProcess::panelSubmit() {
_submitState = SubmitState::Validation;
_form->validateInformation(_form->savedInformation());
return;
} else if (native
&& !native.newCredentials
&& !native.savedCredentials) {
editPaymentMethod();
return;
}
_submitState = SubmitState::Finishing;
_webviewWindow = std::make_unique<Ui::WebviewWindow>(
webviewDataPath(),
_form->details().url,
panelDelegate());
if (!_webviewWindow->shown()) {
// #TODO payments errors
if (!native) {
_webviewWindow = std::make_unique<Ui::WebviewWindow>(
webviewDataPath(),
_form->details().url,
panelDelegate());
if (!_webviewWindow->shown()) {
// #TODO payments errors
}
} else if (native.newCredentials) {
_form->send(native.newCredentials.data);
}
}
@ -316,30 +337,82 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {
return false;
}
void CheckoutProcess::panelEditPaymentMethod() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
editPaymentMethod();
}
void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) {
Expects(_form->nativePayment().type == NativePayment::Type::Stripe);
Expects(!_form->nativePayment().stripePublishableKey.isEmpty());
if (_stripe) {
return;
}
auto configuration = Stripe::PaymentConfiguration{
.publishableKey = _form->nativePayment().stripePublishableKey,
.companyName = "Telegram",
};
_stripe = std::make_unique<Stripe::APIClient>(std::move(configuration));
auto card = Stripe::CardParams{
.number = data.number,
.expMonth = data.expireMonth,
.expYear = data.expireYear,
.cvc = data.cvc,
.name = data.cardholderName,
.addressZip = data.addressZip,
.addressCountry = data.addressCountry,
};
_stripe->createTokenWithCard(std::move(card), crl::guard(this, [=](
Stripe::Token token,
Stripe::Error error) {
_stripe = nullptr;
if (error) {
int a = 0;
// #TODO payment errors
} else {
_form->setPaymentCredentials({
.title = CardTitle(token.card()),
.data = QJsonDocument(QJsonObject{
{ "type", "card" },
{ "id", token.tokenId() },
}).toJson(QJsonDocument::Compact),
.saveOnServer = false,
});
showForm();
}
}));
}
void CheckoutProcess::panelEditShippingInformation() {
showEditInformation(Ui::EditField::ShippingStreet);
showEditInformation(Ui::InformationField::ShippingStreet);
}
void CheckoutProcess::panelEditName() {
showEditInformation(Ui::EditField::Name);
showEditInformation(Ui::InformationField::Name);
}
void CheckoutProcess::panelEditEmail() {
showEditInformation(Ui::EditField::Email);
showEditInformation(Ui::InformationField::Email);
}
void CheckoutProcess::panelEditPhone() {
showEditInformation(Ui::EditField::Phone);
showEditInformation(Ui::InformationField::Phone);
}
void CheckoutProcess::showForm() {
_panel->showForm(
_form->invoice(),
_form->savedInformation(),
_form->nativePayment().details,
_form->shippingOptions());
}
void CheckoutProcess::showEditInformation(Ui::EditField field) {
void CheckoutProcess::showEditInformation(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
@ -349,11 +422,11 @@ void CheckoutProcess::showEditInformation(Ui::EditField field) {
field);
}
void CheckoutProcess::showEditError(Ui::EditField field) {
void CheckoutProcess::showInformationError(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showEditError(
_panel->showInformationError(
_form->invoice(),
_form->savedInformation(),
field);
@ -363,6 +436,10 @@ void CheckoutProcess::chooseShippingOption() {
_panel->chooseShippingOption(_form->shippingOptions());
}
void CheckoutProcess::editPaymentMethod() {
_panel->choosePaymentMethod(_form->nativePayment().details);
}
void CheckoutProcess::panelChooseShippingOption() {
if (_submitState != SubmitState::None) {
return;

View File

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
namespace Stripe {
class APIClient;
} // namespace Stripe
namespace Main {
class Session;
} // namespace Main
@ -19,7 +23,7 @@ class Session;
namespace Payments::Ui {
class Panel;
class WebviewWindow;
enum class EditField;
enum class InformationField;
} // namespace Payments::Ui
namespace Payments {
@ -57,9 +61,10 @@ private:
void handleError(const Error &error);
void showForm();
void showEditInformation(Ui::EditField field);
void showEditError(Ui::EditField field);
void showEditInformation(Ui::InformationField field);
void showInformationError(Ui::InformationField field);
void chooseShippingOption();
void editPaymentMethod();
void performInitialSilentValidation();
[[nodiscard]] QString webviewDataPath() const;
@ -70,6 +75,7 @@ private:
void panelWebviewMessage(const QJsonDocument &message) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelEditPaymentMethod() override;
void panelEditShippingInformation() override;
void panelEditName() override;
void panelEditEmail() override;
@ -78,12 +84,14 @@ private:
void panelChangeShippingOption(const QString &id) override;
void panelValidateInformation(Ui::RequestedInformation data) override;
void panelValidateCard(Ui::UncheckedCardDetails data) override;
void panelShowBox(object_ptr<Ui::BoxContent> box) override;
const not_null<Main::Session*> _session;
const std::unique_ptr<Form> _form;
const std::unique_ptr<Ui::Panel> _panel;
std::unique_ptr<Ui::WebviewWindow> _webviewWindow;
std::unique_ptr<Stripe::APIClient> _stripe;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;

View File

@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "apiwrap.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
namespace Payments {
namespace {
@ -101,7 +105,7 @@ void Form::processForm(const MTPDpayments_paymentForm &data) {
processSavedCredentials(data);
});
}
fillNativePaymentInformation();
_updates.fire({ FormReady{} });
}
@ -152,10 +156,65 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
void Form::processSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data) {
_savedCredentials = Ui::SavedCredentials{
.id = qs(data.vid()),
.title = qs(data.vtitle()),
// #TODO payments not yet supported
//_nativePayment.savedCredentials = SavedCredentials{
// .id = qs(data.vid()),
// .title = qs(data.vtitle()),
//};
refreshNativePaymentDetails();
}
void Form::refreshNativePaymentDetails() {
const auto &saved = _nativePayment.savedCredentials;
const auto &entered = _nativePayment.newCredentials;
_nativePayment.details.credentialsTitle = entered
? entered.title
: saved.title;
_nativePayment.details.ready = entered || saved;
}
void Form::fillNativePaymentInformation() {
auto saved = std::move(_nativePayment.savedCredentials);
auto entered = std::move(_nativePayment.newCredentials);
_nativePayment = NativePayment();
if (_details.nativeProvider != "stripe") {
return;
}
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(
_details.nativeParamsJson,
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payment Error: Could not decode native_params, error %1: %2"
).arg(error.error
).arg(error.errorString()));
return;
} else if (!document.isObject()) {
LOG(("Payment Error: Not an object in native_params."));
return;
}
const auto object = document.object();
const auto value = [&](QStringView key) {
return object.value(key);
};
const auto key = value(u"publishable_key").toString();
if (key.isEmpty()) {
LOG(("Payment Error: No publishable_key in native_params."));
return;
}
_nativePayment = NativePayment{
.type = NativePayment::Type::Stripe,
.stripePublishableKey = key,
.savedCredentials = std::move(saved),
.newCredentials = std::move(entered),
.details = Ui::NativePaymentDetails{
.supported = true,
.needCountry = value(u"need_country").toBool(),
.needZip = value(u"need_zip").toBool(),
.needCardholderName = value(u"need_cardholder_name").toBool(),
},
};
refreshNativePaymentDetails();
}
void Form::send(const QByteArray &serializedCredentials) {
@ -221,6 +280,13 @@ void Form::validateInformation(const Ui::RequestedInformation &information) {
}).send();
}
void Form::setPaymentCredentials(const NewCredentials &credentials) {
Expects(!credentials.empty());
_nativePayment.newCredentials = credentials;
refreshNativePaymentDetails();
}
void Form::setShippingOption(const QString &id) {
_shippingOptions.selectedId = id;
}

View File

@ -33,6 +33,50 @@ struct FormDetails {
}
};
struct SavedCredentials {
QString id;
QString title;
[[nodiscard]] bool valid() const {
return !id.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct NewCredentials {
QString title;
QByteArray data;
bool saveOnServer = false;
[[nodiscard]] bool empty() const {
return data.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct NativePayment {
enum class Type {
None,
Stripe,
};
Type type = Type::None;
QString stripePublishableKey;
SavedCredentials savedCredentials;
NewCredentials newCredentials;
Ui::NativePaymentDetails details;
[[nodiscard]] bool valid() const {
return (type != Type::None);
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct FormReady {};
struct ValidateFinished {};
struct Error {
@ -73,8 +117,8 @@ public:
[[nodiscard]] const Ui::RequestedInformation &savedInformation() const {
return _savedInformation;
}
[[nodiscard]] const Ui::SavedCredentials &savedCredentials() const {
return _savedCredentials;
[[nodiscard]] const NativePayment &nativePayment() const {
return _nativePayment;
}
[[nodiscard]] const Ui::ShippingOptions &shippingOptions() const {
return _shippingOptions;
@ -85,6 +129,7 @@ public:
}
void validateInformation(const Ui::RequestedInformation &information);
void setPaymentCredentials(const NewCredentials &credentials);
void setShippingOption(const QString &id);
void send(const QByteArray &serializedCredentials);
@ -97,6 +142,8 @@ private:
void processSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data);
void processShippingOptions(const QVector<MTPShippingOption> &data);
void fillNativePaymentInformation();
void refreshNativePaymentDetails();
const not_null<Main::Session*> _session;
MTP::Sender _api;
@ -105,7 +152,7 @@ private:
Ui::Invoice _invoice;
FormDetails _details;
Ui::RequestedInformation _savedInformation;
Ui::SavedCredentials _savedCredentials;
NativePayment _nativePayment;
Ui::RequestedInformation _validatedInformation;
mtpRequestId _validateRequestId = 0;

View File

@ -65,7 +65,7 @@ void APIClient::createTokenWithCard(
CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(
FormEncoder::formEncodedDataForObject(card),
FormEncoder::formEncodedDataForObject(MakeEncodable(card)),
std::move(completion));
}

View File

@ -10,25 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "stripe/stripe_decode.h"
namespace Stripe {
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);
}
[[nodiscard]] CardBrand BrandFromString(const QString &brand) {
CardBrand BrandFromString(const QString &brand) {
if (brand == "visa") {
return CardBrand::Visa;
} else if (brand == "american express") {
@ -46,7 +30,7 @@ Card Card::Empty() {
}
}
[[nodiscard]] CardFundingType FundingFromString(const QString &funding) {
CardFundingType FundingFromString(const QString &funding) {
if (funding == "credit") {
return CardFundingType::Credit;
} else if (funding == "debit") {
@ -58,6 +42,25 @@ Card Card::Empty() {
}
}
} // 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",
@ -80,7 +83,7 @@ Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
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"));
result._funding = FundingFromString(string(u"funding").toLower());
result._fingerprint = string(u"fingerprint");
result._country = string(u"country");
result._currency = string(u"currency");
@ -97,6 +100,74 @@ Card Card::DecodedObjectFromAPIResponse(QJsonObject 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();
}

View File

@ -42,6 +42,24 @@ public:
[[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();

View File

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Stripe {
QString CardParams::RootObjectName() const {
QString CardParams::rootObjectName() {
return "card";
}

View File

@ -11,11 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Stripe {
class CardParams final : public FormEncodable {
public:
QString RootObjectName() const override;
std::map<QString, QString> formFieldValues() const override;
struct CardParams {
QString number;
quint32 expMonth = 0;
quint32 expYear = 0;
@ -28,6 +24,9 @@ public:
QString addressZip;
QString addressCountry;
QString currency;
[[nodiscard]] static QString rootObjectName();
[[nodiscard]] std::map<QString, QString> formFieldValues() const;
};
} // namespace Stripe

View File

@ -14,11 +14,26 @@ namespace Stripe {
class FormEncodable {
public:
[[nodiscard]] virtual QString RootObjectName() const = 0;
[[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;
// TODO incomplete, not used: nested complex structures not supported.
[[nodiscard]] virtual std::map<QString, QString> formFieldValues() const
= 0;
};
} // namespace Stripe

View File

@ -13,8 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Stripe {
QByteArray FormEncoder::formEncodedDataForObject(
FormEncodable &object) {
const auto root = object.RootObjectName();
FormEncodable &&object) {
const auto root = object.rootObjectName();
const auto values = object.formFieldValues();
auto result = QByteArray();
auto keys = std::vector<QString>();

View File

@ -14,7 +14,7 @@ namespace Stripe {
class FormEncoder {
public:
[[nodiscard]] static QByteArray formEncodedDataForObject(
FormEncodable &object);
FormEncodable &&object);
};

View File

@ -16,8 +16,11 @@ namespace Stripe {
struct PaymentConfiguration {
QString publishableKey;
// PaymentMethodType additionalPaymentMethods; // Apply Pay
BillingAddressFields requiredBillingAddressFields
= BillingAddressFields::None;
// TODO incomplete, not used.
//BillingAddressFields requiredBillingAddressFields
// = BillingAddressFields::None;
QString companyName;
// QString appleMerchantIdentifier; // Apple Pay
// bool smsAutofillDisabled = true; // Mobile only

View File

@ -0,0 +1,245 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "payments/ui/payments_edit_card.h"
#include "payments/ui/payments_panel_delegate.h"
#include "passport/ui/passport_details_row.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "lang/lang_keys.h"
#include "styles/style_payments.h"
#include "styles/style_passport.h"
namespace Payments::Ui {
namespace {
constexpr auto kMaxPostcodeSize = 10;
[[nodiscard]] uint32 ExtractYear(const QString &value) {
return value.split('/').value(1).toInt() + 2000;
}
[[nodiscard]] uint32 ExtractMonth(const QString &value) {
return value.split('/').value(0).toInt();
}
} // namespace
EditCard::EditCard(
QWidget *parent,
const NativePaymentDetails &native,
CardField field,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _native(native)
, _scroll(this, st::passportPanelScroll)
, _topShadow(this)
, _bottomShadow(this)
, _done(
this,
tr::lng_about_done(),
st::passportPanelSaveValue) {
setupControls();
}
void EditCard::setFocus(CardField field) {
_focusField = field;
if (const auto control = controlForField(field)) {
_scroll->ensureWidgetVisible(control);
control->setFocusFast();
}
}
void EditCard::showError(CardField field) {
if (const auto control = controlForField(field)) {
_scroll->ensureWidgetVisible(control);
control->showError(QString());
}
}
void EditCard::setupControls() {
const auto inner = setupContent();
_done->addClickHandler([=] {
_delegate->panelValidateCard(collect());
});
using namespace rpl::mappers;
_topShadow->toggleOn(
_scroll->scrollTopValue() | rpl::map(_1 > 0));
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
inner->heightValue(),
_1 + _2 < _3));
}
not_null<RpWidget*> EditCard::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::start_with_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
const auto showBox = [=](object_ptr<BoxContent> box) {
_delegate->panelShowBox(std::move(box));
};
using Type = Passport::Ui::PanelDetailsType;
auto maxLabelWidth = 0;
accumulate_max(
maxLabelWidth,
Row::LabelWidth("Card Number"));
accumulate_max(
maxLabelWidth,
Row::LabelWidth("CVC"));
accumulate_max(
maxLabelWidth,
Row::LabelWidth("MM/YY"));
if (_native.needCardholderName) {
accumulate_max(
maxLabelWidth,
Row::LabelWidth("Cardholder Name"));
}
if (_native.needCountry) {
accumulate_max(
maxLabelWidth,
Row::LabelWidth("Billing Country"));
}
if (_native.needZip) {
accumulate_max(
maxLabelWidth,
Row::LabelWidth("Billing Zip"));
}
_number = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Text,
"Card Number",
maxLabelWidth,
QString(),
QString(),
1024));
_cvc = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Text,
"CVC",
maxLabelWidth,
QString(),
QString(),
1024));
_expire = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Text,
"MM/YY",
maxLabelWidth,
QString(),
QString(),
1024));
if (_native.needCardholderName) {
_name = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Text,
"Cardholder Name",
maxLabelWidth,
QString(),
QString(),
1024));
}
if (_native.needCountry) {
_country = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Country,
"Billing Country",
maxLabelWidth,
QString(),
QString()));
}
if (_native.needZip) {
_zip = inner->add(
Row::Create(
inner,
showBox,
QString(),
Type::Postcode,
"Billing Zip Code",
maxLabelWidth,
QString(),
QString(),
kMaxPostcodeSize));
}
return inner;
}
void EditCard::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void EditCard::focusInEvent(QFocusEvent *e) {
if (const auto control = controlForField(_focusField)) {
control->setFocusFast();
}
}
void EditCard::updateControlsGeometry() {
const auto submitTop = height() - _done->height();
_scroll->setGeometry(0, 0, width(), submitTop);
_topShadow->resizeToWidth(width());
_topShadow->moveToLeft(0, 0);
_bottomShadow->resizeToWidth(width());
_bottomShadow->moveToLeft(0, submitTop - st::lineWidth);
_done->setFullWidth(width());
_done->moveToLeft(0, submitTop);
_scroll->updateBars();
}
auto EditCard::controlForField(CardField field) const -> Row* {
switch (field) {
case CardField::Number: return _number;
case CardField::CVC: return _cvc;
case CardField::ExpireDate: return _expire;
case CardField::Name: return _name;
case CardField::AddressCountry: return _country;
case CardField::AddressZip: return _zip;
}
Unexpected("Unknown field in EditCard::controlForField.");
}
UncheckedCardDetails EditCard::collect() const {
return {
.number = _number ? _number->valueCurrent() : QString(),
.cvc = _cvc ? _cvc->valueCurrent() : QString(),
.expireYear = _expire ? ExtractYear(_expire->valueCurrent()) : 0,
.expireMonth = _expire ? ExtractMonth(_expire->valueCurrent()) : 0,
.cardholderName = _name ? _name->valueCurrent() : QString(),
.addressCountry = _country ? _country->valueCurrent() : QString(),
.addressZip = _zip ? _zip->valueCurrent() : QString(),
};
}
} // namespace Payments::Ui

View File

@ -0,0 +1,73 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
} // namespace Ui
namespace Passport::Ui {
class PanelDetailsRow;
} // namespace Passport::Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class EditCard final : public RpWidget {
public:
EditCard(
QWidget *parent,
const NativePaymentDetails &native,
CardField field,
not_null<PanelDelegate*> delegate);
void showError(CardField field);
void setFocus(CardField field);
private:
using Row = Passport::Ui::PanelDetailsRow;
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Row *controlForField(CardField field) const;
[[nodiscard]] UncheckedCardDetails collect() const;
const not_null<PanelDelegate*> _delegate;
NativePaymentDetails _native;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _done;
Row *_number = nullptr;
Row *_cvc = nullptr;
Row *_expire = nullptr;
Row *_name = nullptr;
Row *_country = nullptr;
Row *_zip = nullptr;
CardField _focusField = CardField::Number;
};
} // namespace Payments::Ui

View File

@ -33,7 +33,7 @@ EditInformation::EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
EditField field,
InformationField field,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _invoice(invoice)
@ -48,7 +48,7 @@ EditInformation::EditInformation(
setupControls();
}
void EditInformation::setFocus(EditField field) {
void EditInformation::setFocus(InformationField field) {
_focusField = field;
if (const auto control = controlForField(field)) {
_scroll->ensureWidgetVisible(control);
@ -56,7 +56,7 @@ void EditInformation::setFocus(EditField field) {
}
}
void EditInformation::showError(EditField field) {
void EditInformation::showError(InformationField field) {
if (const auto control = controlForField(field)) {
_scroll->ensureWidgetVisible(control);
control->showError(QString());
@ -264,16 +264,16 @@ void EditInformation::updateControlsGeometry() {
_scroll->updateBars();
}
auto EditInformation::controlForField(EditField field) const -> Row* {
auto EditInformation::controlForField(InformationField field) const -> Row* {
switch (field) {
case EditField::ShippingStreet: return _street1;
case EditField::ShippingCity: return _city;
case EditField::ShippingState: return _state;
case EditField::ShippingCountry: return _country;
case EditField::ShippingPostcode: return _postcode;
case EditField::Name: return _name;
case EditField::Email: return _email;
case EditField::Phone: return _phone;
case InformationField::ShippingStreet: return _street1;
case InformationField::ShippingCity: return _city;
case InformationField::ShippingState: return _state;
case InformationField::ShippingCountry: return _country;
case InformationField::ShippingPostcode: return _postcode;
case InformationField::Name: return _name;
case InformationField::Email: return _email;
case InformationField::Phone: return _phone;
}
Unexpected("Unknown field in EditInformation::controlForField.");
}

View File

@ -33,11 +33,11 @@ public:
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
EditField field,
InformationField field,
not_null<PanelDelegate*> delegate);
void showError(EditField field);
void setFocus(EditField field);
void showError(InformationField field);
void setFocus(InformationField field);
private:
using Row = Passport::Ui::PanelDetailsRow;
@ -48,7 +48,7 @@ private:
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Row *controlForField(EditField field) const;
[[nodiscard]] Row *controlForField(InformationField field) const;
[[nodiscard]] RequestedInformation collect() const;
@ -71,7 +71,7 @@ private:
Row *_email = nullptr;
Row *_phone = nullptr;
EditField _focusField = EditField::ShippingStreet;
InformationField _focusField = InformationField::ShippingStreet;
};

View File

@ -30,10 +30,12 @@ FormSummary::FormSummary(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const NativePaymentDetails &native,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _invoice(invoice)
, _native(native)
, _options(options)
, _information(current)
, _scroll(this, st::passportPanelScroll)
@ -134,6 +136,20 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
st::passportFormDividerHeight),
{ 0, 0, 0, st::passportFormHeaderPadding.top() });
if (_native.supported) {
const auto method = inner->add(object_ptr<FormRow>(inner));
method->addClickHandler([=] {
_delegate->panelEditPaymentMethod();
});
method->updateContent(
tr::lng_payments_payment_method(tr::now),
(_native.ready
? _native.credentialsTitle
: tr::lng_payments_payment_method_ph(tr::now)),
_native.ready,
false,
anim::type::instant);
}
if (_invoice.isShippingAddressRequested) {
const auto info = inner->add(object_ptr<FormRow>(inner));
info->addClickHandler([=] {
@ -153,7 +169,9 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
push(_information.shippingAddress.postcode);
info->updateContent(
tr::lng_payments_shipping_address(tr::now),
(list.isEmpty() ? "enter pls" : list.join(", ")),
(list.isEmpty()
? tr::lng_payments_shipping_address_ph(tr::now)
: list.join(", ")),
!list.isEmpty(),
false,
anim::type::instant);
@ -167,7 +185,7 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
tr::lng_payments_shipping_method(tr::now),
(selected != end(_options.list)
? selected->title
: "enter pls"),
: tr::lng_payments_shipping_method_ph(tr::now)),
(selected != end(_options.list)),
false,
anim::type::instant);
@ -178,7 +196,7 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
name->updateContent(
tr::lng_payments_info_name(tr::now),
(_information.name.isEmpty()
? "enter pls"
? tr::lng_payments_info_name_ph(tr::now)
: _information.name),
!_information.name.isEmpty(),
false,
@ -190,7 +208,7 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
email->updateContent(
tr::lng_payments_info_email(tr::now),
(_information.email.isEmpty()
? "enter pls"
? tr::lng_payments_info_email_ph(tr::now)
: _information.email),
!_information.email.isEmpty(),
false,
@ -202,7 +220,7 @@ not_null<Ui::RpWidget*> FormSummary::setupContent() {
phone->updateContent(
tr::lng_payments_info_phone(tr::now),
(_information.phone.isEmpty()
? "enter pls"
? tr::lng_payments_info_phone_ph(tr::now)
: _information.phone),
!_information.phone.isEmpty(),
false,

View File

@ -29,6 +29,7 @@ public:
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const NativePaymentDetails &native,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate);
@ -44,6 +45,7 @@ private:
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
NativePaymentDetails _native;
ShippingOptions _options;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/ui/payments_form_summary.h"
#include "payments/ui/payments_edit_information.h"
#include "payments/ui/payments_edit_card.h"
#include "payments/ui/payments_panel_delegate.h"
#include "ui/widgets/separate_panel.h"
#include "ui/boxes/single_choice_box.h"
@ -45,12 +46,14 @@ void Panel::requestActivate() {
void Panel::showForm(
const Invoice &invoice,
const RequestedInformation &current,
const NativePaymentDetails &native,
const ShippingOptions &options) {
_widget->showInner(
base::make_unique_q<FormSummary>(
_widget.get(),
invoice,
current,
native,
options,
_delegate));
_widget->setBackAllowed(false);
@ -59,29 +62,30 @@ void Panel::showForm(
void Panel::showEditInformation(
const Invoice &invoice,
const RequestedInformation &current,
EditField field) {
InformationField field) {
auto edit = base::make_unique_q<EditInformation>(
_widget.get(),
invoice,
current,
field,
_delegate);
_weakEditWidget = edit.get();
_weakEditInformation = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditWidget->setFocus(field);
_weakEditInformation->setFocus(field);
}
void Panel::showEditError(
void Panel::showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
EditField field) {
if (_weakEditWidget) {
_weakEditWidget->showError(field);
InformationField field) {
if (_weakEditInformation) {
_weakEditInformation->showError(field);
} else {
showEditInformation(invoice, current, field);
if (_weakEditWidget && field == EditField::ShippingCountry) {
_weakEditWidget->showError(field);
if (_weakEditInformation
&& field == InformationField::ShippingCountry) {
_weakEditInformation->showError(field);
}
}
}
@ -109,6 +113,57 @@ void Panel::chooseShippingOption(const ShippingOptions &options) {
}));
}
void Panel::choosePaymentMethod(const NativePaymentDetails &native) {
Expects(native.supported);
if (!native.ready) {
showEditCard(native, CardField::Number);
return;
}
const auto title = native.credentialsTitle;
showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto save = [=](int option) {
if (option) {
showEditCard(native, CardField::Number);
}
};
SingleChoiceBox(box, {
.title = tr::lng_payments_payment_method(),
.options = { native.credentialsTitle, "New Card..." }, // #TODO payments lang
.initialSelection = 0,
.callback = save,
});
}));
}
void Panel::showEditCard(
const NativePaymentDetails &native,
CardField field) {
auto edit = base::make_unique_q<EditCard>(
_widget.get(),
native,
field,
_delegate);
_weakEditCard = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditCard->setFocus(field);
}
void Panel::showCardError(
const NativePaymentDetails &native,
CardField field) {
if (_weakEditCard) {
_weakEditCard->showError(field);
} else {
showEditCard(native, field);
if (_weakEditCard
&& field == CardField::AddressCountry) {
_weakEditCard->showError(field);
}
}
}
rpl::producer<> Panel::backRequests() const {
return _widget->backRequests();
}

View File

@ -22,8 +22,11 @@ class PanelDelegate;
struct Invoice;
struct RequestedInformation;
struct ShippingOptions;
enum class EditField;
enum class InformationField;
enum class CardField;
class EditInformation;
class EditCard;
struct NativePaymentDetails;
class Panel final {
public:
@ -35,16 +38,24 @@ public:
void showForm(
const Invoice &invoice,
const RequestedInformation &current,
const NativePaymentDetails &native,
const ShippingOptions &options);
void showEditInformation(
const Invoice &invoice,
const RequestedInformation &current,
EditField field);
void showEditError(
InformationField field);
void showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
EditField field);
InformationField field);
void showEditCard(
const NativePaymentDetails &native,
CardField field);
void showCardError(
const NativePaymentDetails &native,
CardField field);
void chooseShippingOption(const ShippingOptions &options);
void choosePaymentMethod(const NativePaymentDetails &native);
[[nodiscard]] rpl::producer<> backRequests() const;
@ -56,7 +67,8 @@ public:
private:
const not_null<PanelDelegate*> _delegate;
std::unique_ptr<SeparatePanel> _widget;
QPointer<EditInformation> _weakEditWidget;
QPointer<EditInformation> _weakEditInformation;
QPointer<EditCard> _weakEditCard;
};

View File

@ -104,19 +104,7 @@ struct RequestedInformation {
}
};
struct SavedCredentials {
QString id;
QString title;
[[nodiscard]] bool valid() const {
return !id.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
enum class EditField {
enum class InformationField {
ShippingStreet,
ShippingCity,
ShippingState,
@ -127,4 +115,32 @@ enum class EditField {
Phone,
};
struct NativePaymentDetails {
QString credentialsTitle;
bool ready = false;
bool supported = false;
bool needCountry = false;
bool needZip = false;
bool needCardholderName = false;
};
enum class CardField {
Number,
CVC,
ExpireDate,
Name,
AddressCountry,
AddressZip,
};
struct UncheckedCardDetails {
QString number;
QString cvc;
uint32 expireYear = 0;
uint32 expireMonth = 0;
QString cardholderName;
QString addressCountry;
QString addressZip;
};
} // namespace Payments::Ui

View File

@ -21,6 +21,7 @@ namespace Payments::Ui {
using namespace ::Ui;
struct RequestedInformation;
struct UncheckedCardDetails;
class PanelDelegate {
public:
@ -30,6 +31,7 @@ public:
virtual void panelWebviewMessage(const QJsonDocument &message) = 0;
virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;
virtual void panelEditPaymentMethod() = 0;
virtual void panelEditShippingInformation() = 0;
virtual void panelEditName() = 0;
virtual void panelEditEmail() = 0;
@ -38,6 +40,7 @@ public:
virtual void panelChangeShippingOption(const QString &id) = 0;
virtual void panelValidateInformation(RequestedInformation data) = 0;
virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0;
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
};

View File

@ -69,6 +69,8 @@ PRIVATE
passport/ui/passport_form_row.cpp
passport/ui/passport_form_row.h
payments/ui/payments_edit_card.cpp
payments/ui/payments_edit_card.h
payments/ui/payments_edit_information.cpp
payments/ui/payments_edit_information.h
payments/ui/payments_form_summary.cpp