Allow saving and using saved credentials.

This commit is contained in:
John Preston 2021-03-31 21:15:49 +04:00
parent 2e58993181
commit 663db64688
16 changed files with 350 additions and 55 deletions

View file

@ -1880,15 +1880,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_info_email" = "Email";
"lng_payments_info_phone" = "Phone";
"lng_payments_shipping_address_title" = "Shipping Information";
"lng_payments_save_shipping_about" = "You can save your shipping information for future use.";
"lng_payments_card_title" = "New Card";
"lng_payments_card_number" = "Card Number";
"lng_payments_card_holder" = "Cardholder name";
"lng_payments_billing_address" = "Billing Information";
"lng_payments_billing_country" = "Country";
"lng_payments_billing_zip_code" = "Zip Code";
"lng_payments_save_payment_about" = "You can save your payment information for future use.";
"lng_payments_save_information" = "Save Information for future use";
"lng_payments_need_password" = "You can save your payment information for future use. Please turn on Two-Step Verification to enable this.";
"lng_payments_password_title" = "Payment Confirmation";
"lng_payments_password_description" = "Your card {card} is on file. To pay with this card, please enter your 2-Step-Verification password.";
"lng_payments_password_submit" = "Pay";
"lng_payments_tips_label" = "Tips";
"lng_payments_tips_title" = "Tips";
"lng_payments_tips_enter" = "Enter tips amount";

View file

@ -361,7 +361,11 @@ void PasscodeBox::closeReplacedBy() {
}
void PasscodeBox::setPasswordFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
setPasswordFail(error.type());
}
void PasscodeBox::setPasswordFail(const QString &type) {
if (MTP::IsFloodError(type)) {
closeReplacedBy();
_setRequest = 0;
@ -378,20 +382,19 @@ void PasscodeBox::setPasswordFail(const MTP::Error &error) {
closeReplacedBy();
_setRequest = 0;
const auto &err = error.type();
if (err == qstr("PASSWORD_HASH_INVALID")
|| err == qstr("SRP_PASSWORD_CHANGED")) {
if (type == qstr("PASSWORD_HASH_INVALID")
|| type == qstr("SRP_PASSWORD_CHANGED")) {
if (_oldPasscode->isHidden()) {
_passwordReloadNeeded.fire({});
closeBox();
} else {
badOldPasscode();
}
} else if (err == qstr("SRP_ID_INVALID")) {
} else if (type == qstr("SRP_ID_INVALID")) {
handleSrpIdInvalid();
//} else if (err == qstr("NEW_PASSWORD_BAD")) {
//} else if (err == qstr("NEW_SALT_INVALID")) {
} else if (err == qstr("EMAIL_INVALID")) {
//} else if (type == qstr("NEW_PASSWORD_BAD")) {
//} else if (type == qstr("NEW_SALT_INVALID")) {
} else if (type == qstr("EMAIL_INVALID")) {
_emailError = tr::lng_cloud_password_bad_email(tr::now);
_recoverEmail->setFocus();
_recoverEmail->showError();
@ -682,12 +685,15 @@ void PasscodeBox::serverError() {
}
bool PasscodeBox::handleCustomCheckError(const MTP::Error &error) {
const auto &type = error.type();
if (MTP::IsFloodError(error)
return handleCustomCheckError(error.type());
}
bool PasscodeBox::handleCustomCheckError(const QString &type) {
if (MTP::IsFloodError(type)
|| type == qstr("PASSWORD_HASH_INVALID")
|| type == qstr("SRP_PASSWORD_CHANGED")
|| type == qstr("SRP_ID_INVALID")) {
setPasswordFail(error);
setPasswordFail(type);
return true;
}
return false;

View file

@ -56,6 +56,7 @@ public:
rpl::producer<> clearUnconfirmedPassword() const;
bool handleCustomCheckError(const MTP::Error &error);
bool handleCustomCheckError(const QString &type);
protected:
void prepare() override;
@ -82,6 +83,7 @@ private:
void setPasswordDone(const QByteArray &newPasswordBytes);
void setPasswordFail(const MTP::Error &error);
void setPasswordFail(const QString &type);
void setPasswordFail(
const QByteArray &newPasswordBytes,
const QString &email,

View file

@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_lock_widgets.h"
#include "window/themes/window_theme.h"
//#include "platform/platform_specific.h"
#include "base/unixtime.h"
#include "calls/calls_instance.h"
#include "support/support_helper.h"
#include "facades.h"
@ -43,6 +44,7 @@ namespace Main {
namespace {
constexpr auto kLegacyCallsPeerToPeerNobody = 4;
constexpr auto kTmpPasswordReserveTime = TimeId(10);
[[nodiscard]] QString ValidatedInternalLinksDomain(
not_null<const Session*> session) {
@ -155,6 +157,20 @@ Session::Session(
_api->requestNotifySettings(MTP_inputNotifyBroadcasts());
}
void Session::setTmpPassword(const QByteArray &password, TimeId validUntil) {
if (_tmpPassword.isEmpty() || validUntil > _tmpPasswordValidUntil) {
_tmpPassword = password;
_tmpPasswordValidUntil = validUntil;
}
}
QByteArray Session::validTmpPassword() const {
return (_tmpPasswordValidUntil
>= base::unixtime::now() + kTmpPasswordReserveTime)
? _tmpPassword
: QByteArray();
}
// Can be called only right before ~Session.
void Session::finishLogout() {
updates().updateOnline();

View file

@ -145,6 +145,9 @@ public:
[[nodiscard]] QString createInternalLink(const QString &query) const;
[[nodiscard]] QString createInternalLinkFull(const QString &query) const;
void setTmpPassword(const QByteArray &password, TimeId validUntil);
[[nodiscard]] QByteArray validTmpPassword() const;
// Can be called only right before ~Session.
void finishLogout();
@ -190,6 +193,9 @@ private:
base::flat_set<not_null<Window::SessionController*>> _windows;
base::Timer _saveSettingsTimer;
QByteArray _tmpPassword;
TimeId _tmpPasswordValidUntil = 0;
rpl::lifetime _lifetime;
};

View file

@ -38,8 +38,12 @@ private:
};
inline bool IsFloodError(const QString &type) {
return type.startsWith(qstr("FLOOD_WAIT_"));
}
inline bool IsFloodError(const Error &error) {
return error.type().startsWith(qstr("FLOOD_WAIT_"));
return IsFloodError(error.type());
}
inline bool IsTemporaryError(const Error &error) {

View file

@ -16,8 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history.h"
#include "data/data_user.h" // UserData::isBot.
#include "boxes/passcode_box.h"
#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
#include "core/file_utilities.h" // File::OpenUrl.
#include "core/core_cloud_password.h" // Core::CloudPasswordState
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include <QJsonDocument>
@ -104,11 +107,19 @@ CheckoutProcess::CheckoutProcess(
) | rpl::start_with_next([=](const FormUpdate &update) {
handleFormUpdate(update);
}, _lifetime);
_panel->backRequests(
) | rpl::start_with_next([=] {
panelCancelEdit();
}, _panel->lifetime());
showForm();
if (mode == Mode::Payment) {
_session->api().passwordState(
) | rpl::start_with_next([=](const Core::CloudPasswordState &state) {
_form->setHasPassword(!!state.request);
}, _lifetime);
}
}
CheckoutProcess::~CheckoutProcess() {
@ -132,6 +143,9 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) {
if (!_initialSilentValidation) {
showForm();
}
if (_form->paymentMethod().savedCredentials) {
_session->api().reloadPasswordState();
}
}, [&](const ThumbnailUpdated &data) {
_panel->updateFormThumbnail(data.thumbnail);
}, [&](const ValidateFinished &) {
@ -139,12 +153,19 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) {
_initialSilentValidation = false;
}
showForm();
if (_submitState == SubmitState::Validation) {
_submitState = SubmitState::Validated;
const auto submitted = (_submitState == SubmitState::Validating);
_submitState = SubmitState::Validated;
if (submitted) {
panelSubmit();
}
}, [&](const PaymentMethodUpdate &) {
}, [&](const PaymentMethodUpdate &data) {
showForm();
if (data.requestNewPassword) {
requestSetPassword();
}
}, [&](const TmpPasswordRequired &) {
_submitState = SubmitState::Validated;
requestPassword();
}, [&](const VerificationNeeded &data) {
if (!_panel->showWebview(data.url, false)) {
File::OpenUrl(data.url);
@ -176,7 +197,7 @@ void CheckoutProcess::handleError(const Error &error) {
}
break;
case Error::Type::Validate: {
if (_submitState == SubmitState::Validation
if (_submitState == SubmitState::Validating
|| _submitState == SubmitState::Validated) {
_submitState = SubmitState::None;
}
@ -243,9 +264,19 @@ void CheckoutProcess::handleError(const Error &error) {
showToast({ "Error: " + id });
}
} break;
case Error::Type::TmpPassword:
if (const auto box = _enterPasswordBox.data()) {
if (!box->handleCustomCheckError(id)) {
showToast({ "Error: Could not generate tmp password." });
}
}
break;
case Error::Type::Send:
if (const auto box = _enterPasswordBox.data()) {
box->closeBox();
}
if (_submitState == SubmitState::Finishing) {
_submitState = SubmitState::None;
_submitState = SubmitState::Validated;
}
if (id == u"PAYMENT_FAILED"_q) {
showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message
@ -256,6 +287,8 @@ void CheckoutProcess::handleError(const Error &error) {
|| id == u"PAYMENT_CREDENTIALS_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) {
showToast({ "Error: " + id + ". Your card has not been billed." });
} else if (id == u"TMP_PASSWORD_INVALID"_q) {
// #TODO payments save
} else {
showToast({ "Error: " + id });
}
@ -301,7 +334,7 @@ void CheckoutProcess::panelSubmit() {
if (_form->invoice().receipt.paid) {
closeAndReactivate();
return;
} else if (_submitState == SubmitState::Validation
} else if (_submitState == SubmitState::Validating
|| _submitState == SubmitState::Finishing) {
return;
}
@ -310,25 +343,25 @@ void CheckoutProcess::panelSubmit() {
const auto &options = _form->shippingOptions();
if (!options.list.empty() && options.selectedId.isEmpty()) {
chooseShippingOption();
return;
} else if (_submitState != SubmitState::Validated
&& options.list.empty()
&& (invoice.isShippingAddressRequested
|| invoice.isNameRequested
|| invoice.isEmailRequested
|| invoice.isPhoneRequested)) {
_submitState = SubmitState::Validation;
_submitState = SubmitState::Validating;
_form->validateInformation(_form->savedInformation());
return;
} else if (!method.newCredentials && !method.savedCredentials) {
editPaymentMethod();
return;
} else {
_submitState = SubmitState::Finishing;
_form->submit();
}
_submitState = SubmitState::Finishing;
_form->submit();
}
void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) {
void CheckoutProcess::panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) {
if (!message.isArray()) {
LOG(("Payments Error: "
"Not an array received in buy_callback arguments."));
@ -371,7 +404,7 @@ void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) {
.data = QJsonDocument(
credentials.toObject()
).toJson(QJsonDocument::Compact),
.saveOnServer = false, // #TODO payments save
.saveOnServer = saveInformation,
});
});
}
@ -399,8 +432,10 @@ void CheckoutProcess::panelEditPaymentMethod() {
editPaymentMethod();
}
void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) {
_form->validateCard(data);
void CheckoutProcess::panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) {
_form->validateCard(data, saveInformation);
}
void CheckoutProcess::panelEditShippingInformation() {
@ -466,6 +501,70 @@ void CheckoutProcess::editPaymentMethod() {
_panel->choosePaymentMethod(_form->paymentMethod().ui);
}
void CheckoutProcess::requestSetPassword() {
_session->api().reloadPasswordState();
_panel->askSetPassword();
}
void CheckoutProcess::requestPassword() {
getPasswordState([=](const Core::CloudPasswordState &state) {
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_payments_password_title();
fields.customDescription = tr::lng_payments_password_description(
tr::now,
lt_card,
_form->paymentMethod().savedCredentials.title);
fields.customSubmitButton = tr::lng_payments_password_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result) {
_form->submit(result);
};
auto owned = Box<PasscodeBox>(_session, fields);
_enterPasswordBox = owned.data();
_panel->showBox(std::move(owned));
});
}
void CheckoutProcess::panelSetPassword() {
getPasswordState([=](const Core::CloudPasswordState &state) {
if (state.request) {
return;
}
auto owned = Box<PasscodeBox>(
_session,
PasscodeBox::CloudFields::From(state));
const auto box = owned.data();
rpl::merge(
box->newPasswordSet() | rpl::to_empty,
box->passwordReloadNeeded()
) | rpl::start_with_next([=] {
_session->api().reloadPasswordState();
}, box->lifetime());
box->clearUnconfirmedPassword(
) | rpl::start_with_next([=] {
_session->api().clearUnconfirmedPassword();
}, box->lifetime());
_panel->showBox(std::move(owned));
});
}
void CheckoutProcess::getPasswordState(
Fn<void(const Core::CloudPasswordState&)> callback) {
Expects(callback != nullptr);
if (_gettingPasswordState) {
return;
}
_session->api().passwordState(
) | rpl::start_with_next([=](const Core::CloudPasswordState &state) {
_gettingPasswordState.destroy();
callback(state);
}, _gettingPasswordState);
}
void CheckoutProcess::panelChooseShippingOption() {
if (_submitState != SubmitState::None) {
return;
@ -492,6 +591,9 @@ void CheckoutProcess::panelChangeTips(int64 value) {
void CheckoutProcess::panelValidateInformation(
Ui::RequestedInformation data) {
if (_submitState == SubmitState::Validated) {
_submitState = SubmitState::None;
}
_form->validateInformation(data);
}

View file

@ -11,11 +11,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/weak_ptr.h"
class HistoryItem;
class PasscodeBox;
namespace Core {
class CloudPasswordState;
} // namespace Core
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Payments::Ui {
class Panel;
enum class InformationField;
@ -55,7 +64,7 @@ public:
private:
enum class SubmitState {
None,
Validation,
Validating,
Validated,
Finishing,
};
@ -77,13 +86,22 @@ private:
void chooseTips();
void editPaymentMethod();
void requestSetPassword();
void requestSetPasswordSure(QPointer<Ui::GenericBox> old);
void requestPassword();
void getPasswordState(
Fn<void(const Core::CloudPasswordState&)> callback);
void performInitialSilentValidation();
void panelRequestClose() override;
void panelCloseSure() override;
void panelSubmit() override;
void panelWebviewMessage(const QJsonDocument &message) override;
void panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelSetPassword() override;
void panelCancelEdit() override;
void panelEditPaymentMethod() override;
@ -97,7 +115,9 @@ private:
void panelChangeTips(int64 value) override;
void panelValidateInformation(Ui::RequestedInformation data) override;
void panelValidateCard(Ui::UncheckedCardDetails data) override;
void panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) override;
void panelShowBox(object_ptr<Ui::BoxContent> box) override;
QString panelWebviewDataPath() override;
@ -105,10 +125,12 @@ private:
const not_null<Main::Session*> _session;
const std::unique_ptr<Form> _form;
const std::unique_ptr<Ui::Panel> _panel;
QPointer<PasscodeBox> _enterPasswordBox;
Fn<void()> _reactivate;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;
rpl::lifetime _gettingPasswordState;
rpl::lifetime _lifetime;
};

View file

@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "stripe/stripe_card_validator.h"
#include "ui/image/image.h"
#include "apiwrap.h"
#include "core/core_cloud_password.h"
#include "styles/style_payments.h" // paymentsThumbnailSize.
#include <QtCore/QJsonDocument>
@ -32,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Payments {
namespace {
constexpr auto kPasswordPeriod = 15 * TimeId(60);
[[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) {
return address.match([](const MTPDpostAddress &data) {
return Ui::Address{
@ -381,11 +384,10 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
void Form::processSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data) {
// #TODO payments save
//_nativePayment.savedCredentials = SavedCredentials{
// .id = qs(data.vid()),
// .title = qs(data.vtitle()),
//};
_paymentMethod.savedCredentials = SavedCredentials{
.id = qs(data.vid()),
.title = qs(data.vtitle()),
};
refreshPaymentMethodDetails();
}
@ -395,6 +397,9 @@ void Form::refreshPaymentMethodDetails() {
_paymentMethod.ui.title = entered ? entered.title : saved.title;
_paymentMethod.ui.ready = entered || saved;
_paymentMethod.ui.native.defaultCountry = defaultCountry();
_paymentMethod.ui.canSaveInformation
= _paymentMethod.ui.native.canSaveInformation
= _details.canSaveCredentials || _details.passwordMissing;
}
QString Form::defaultPhone() const {
@ -452,7 +457,16 @@ void Form::fillStripeNativeMethod() {
}
void Form::submit() {
Expects(!_paymentMethod.newCredentials.data.isEmpty()); // #TODO payments save
Expects(_paymentMethod.newCredentials
|| _paymentMethod.savedCredentials);
const auto password = _paymentMethod.newCredentials
? QByteArray()
: _session->validTmpPassword();
if (!_paymentMethod.newCredentials && password.isEmpty()) {
_updates.fire(TmpPasswordRequired{});
return;
}
using Flag = MTPpayments_SendPaymentForm::Flag;
_api.request(MTPpayments_SendPaymentForm(
@ -468,9 +482,16 @@ void Form::submit() {
MTP_int(_msgId),
MTP_string(_requestedInformationId),
MTP_string(_shippingOptions.selectedId),
MTP_inputPaymentCredentials(
MTP_flags(0),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data))),
(_paymentMethod.newCredentials
? MTP_inputPaymentCredentials(
MTP_flags((_paymentMethod.newCredentials.saveOnServer
&& _details.canSaveCredentials)
? MTPDinputPaymentCredentials::Flag::f_save
: MTPDinputPaymentCredentials::Flag(0)),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))
: MTP_inputPaymentCredentialsSaved(
MTP_string(_paymentMethod.savedCredentials.id),
MTP_bytes(password))),
MTP_long(_invoice.tipsSelected)
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
@ -483,6 +504,27 @@ void Form::submit() {
}).send();
}
void Form::submit(const Core::CloudPasswordResult &result) {
if (_passwordRequestId) {
return;
}
_passwordRequestId = _api.request(MTPaccount_GetTmpPassword(
result.result,
MTP_int(kPasswordPeriod)
)).done([=](const MTPaccount_TmpPassword &result) {
_passwordRequestId = 0;
result.match([&](const MTPDaccount_tmpPassword &data) {
_session->setTmpPassword(
data.vtmp_password().v,
data.vvalid_until().v);
submit();
});
}).fail([=](const MTP::Error &error) {
_passwordRequestId = 0;
_updates.fire(Error{ Error::Type::TmpPassword, error.type() });
}).send();
}
void Form::validateInformation(const Ui::RequestedInformation &information) {
if (_validateRequestId) {
if (_validatedInformation == information) {
@ -573,7 +615,9 @@ Error Form::informationErrorLocal(
return Error();
}
void Form::validateCard(const Ui::UncheckedCardDetails &details) {
void Form::validateCard(
const Ui::UncheckedCardDetails &details,
bool saveInformation) {
Expects(!v::is_null(_paymentMethod.native.data));
if (!validateCardLocal(details)) {
@ -581,7 +625,7 @@ void Form::validateCard(const Ui::UncheckedCardDetails &details) {
}
const auto &native = _paymentMethod.native.data;
if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
validateCard(*stripe, details);
validateCard(*stripe, details, saveInformation);
} else {
Unexpected("Native payment provider in Form::validateCard.");
}
@ -635,7 +679,8 @@ Error Form::cardErrorLocal(const Ui::UncheckedCardDetails &details) const {
void Form::validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details) {
const Ui::UncheckedCardDetails &details,
bool saveInformation) {
Expects(!method.publishableKey.isEmpty());
if (_stripe) {
@ -673,7 +718,7 @@ void Form::validateCard(
{ "type", "card" },
{ "id", token.tokenId() },
}).toJson(QJsonDocument::Compact),
.saveOnServer = false, // #TODO payments save
.saveOnServer = saveInformation,
});
}
}));
@ -683,8 +728,17 @@ void Form::setPaymentCredentials(const NewCredentials &credentials) {
Expects(!credentials.empty());
_paymentMethod.newCredentials = credentials;
const auto requestNewPassword = credentials.saveOnServer
&& !_details.canSaveCredentials
&& _details.passwordMissing;
refreshPaymentMethodDetails();
_updates.fire(PaymentMethodUpdate{});
_updates.fire(PaymentMethodUpdate{ requestNewPassword });
}
void Form::setHasPassword(bool has) {
if (_details.passwordMissing) {
_details.canSaveCredentials = has;
}
}
void Form::setShippingOption(const QString &id) {

View file

@ -13,6 +13,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class Image;
namespace Core {
struct CloudPasswordResult;
} // namespace Core
namespace Stripe {
class APIClient;
} // namespace Stripe
@ -107,10 +111,13 @@ struct ThumbnailUpdated {
QImage thumbnail;
};
struct ValidateFinished {};
struct PaymentMethodUpdate {};
struct PaymentMethodUpdate {
bool requestNewPassword = false;
};
struct VerificationNeeded {
QString url;
};
struct TmpPasswordRequired {};
struct PaymentFinished {
MTPUpdates updates;
};
@ -120,6 +127,7 @@ struct Error {
Form,
Validate,
Stripe,
TmpPassword,
Send,
};
Type type = Type::None;
@ -139,6 +147,7 @@ struct FormUpdate : std::variant<
ValidateFinished,
PaymentMethodUpdate,
VerificationNeeded,
TmpPasswordRequired,
PaymentFinished,
Error> {
using variant::variant;
@ -170,11 +179,15 @@ public:
}
void validateInformation(const Ui::RequestedInformation &information);
void validateCard(const Ui::UncheckedCardDetails &details);
void validateCard(
const Ui::UncheckedCardDetails &details,
bool saveInformation);
void setPaymentCredentials(const NewCredentials &credentials);
void setHasPassword(bool has);
void setShippingOption(const QString &id);
void setTips(int64 value);
void submit();
void submit(const Core::CloudPasswordResult &result);
private:
void fillInvoiceFromMessage();
@ -208,7 +221,8 @@ private:
void validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details);
const Ui::UncheckedCardDetails &details,
bool saveInformation);
bool validateInformationLocal(
const Ui::RequestedInformation &information) const;
@ -235,6 +249,7 @@ private:
Ui::RequestedInformation _validatedInformation;
mtpRequestId _validateRequestId = 0;
mtpRequestId _passwordRequestId = 0;
std::unique_ptr<Stripe::APIClient> _stripe;

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "lang/lang_keys.h"
@ -246,7 +247,7 @@ void EditCard::setupControls() {
const auto inner = setupContent();
_submit->addClickHandler([=] {
_delegate->panelValidateCard(collect());
_delegate->panelValidateCard(collect(), (_save && _save->checked()));
});
_cancel->addClickHandler([=] {
_delegate->panelCancelEdit();
@ -357,6 +358,14 @@ not_null<RpWidget*> EditCard::setupContent() {
}, lifetime());
}
}
if (_native.canSaveInformation) {
_save = inner->add(
object_ptr<Ui::Checkbox>(
inner,
tr::lng_payments_save_information(tr::now),
false),
st::paymentsSaveCheckboxPadding);
}
return inner;
}

View file

@ -15,6 +15,7 @@ namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class Checkbox;
} // namespace Ui
namespace Payments::Ui {
@ -62,6 +63,7 @@ private:
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _zip;
Checkbox *_save = nullptr;
CardField _focusField = CardField::Number;

View file

@ -229,6 +229,18 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
showEditCard(method.native, CardField::Number);
} else if (!showWebview(method.url, true)) {
// #TODO payments errors not supported
} else if (method.canSaveInformation) {
const auto &padding = st::paymentsPanelPadding;
_saveWebviewInformation = CreateChild<Checkbox>(
_webviewBottom.get(),
tr::lng_payments_save_information(tr::now),
false);
const auto height = padding.top()
+ _saveWebviewInformation->heightNoMargins()
+ padding.bottom();
_saveWebviewInformation->moveToLeft(padding.right(), padding.top());
_saveWebviewInformation->show();
_webviewBottom->resize(_webviewBottom->width(), height);
}
}
@ -244,7 +256,17 @@ bool Panel::showWebview(const QString &url, bool allowBack) {
bool Panel::createWebview() {
auto container = base::make_unique_q<RpWidget>(_widget.get());
container->setGeometry(_widget->innerGeometry());
_webviewBottom = std::make_unique<RpWidget>(_widget.get());
const auto bottom = _webviewBottom.get();
bottom->show();
bottom->heightValue(
) | rpl::start_with_next([=, raw = container.get()](int height) {
const auto inner = _widget->innerGeometry();
bottom->move(inner.x(), inner.y() + inner.height() - height);
raw->resize(inner.width(), inner.height() - height);
bottom->resizeToWidth(inner.width());
}, bottom->lifetime());
container->show();
_webview = std::make_unique<Webview::Window>(
@ -257,6 +279,9 @@ bool Panel::createWebview() {
if (_webview.get() == raw) {
_webview = nullptr;
}
if (_webviewBottom.get() == bottom) {
_webviewBottom = nullptr;
}
});
if (!raw->widget()) {
return false;
@ -268,7 +293,9 @@ bool Panel::createWebview() {
}, container->lifetime());
raw->setMessageHandler([=](const QJsonDocument &message) {
_delegate->panelWebviewMessage(message);
const auto save = _saveWebviewInformation
&& _saveWebviewInformation->checked();
_delegate->panelWebviewMessage(message, save);
});
raw->setNavigationHandler([=](const QString &uri) {
@ -308,6 +335,22 @@ void Panel::choosePaymentMethod(const PaymentMethodDetails &method) {
}));
}
void Panel::askSetPassword() {
showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_payments_need_password(),
st::boxLabel),
st::boxPadding);
box->addButton(tr::lng_continue(), [=] {
_delegate->panelSetPassword();
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::showEditCard(
const NativeMethodDetails &native,
CardField field) {

View file

@ -10,8 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
namespace Ui {
class RpWidget;
class SeparatePanel;
class BoxContent;
class Checkbox;
} // namespace Ui
namespace Webview {
@ -65,6 +67,7 @@ public:
void chooseShippingOption(const ShippingOptions &options);
void chooseTips(const Invoice &invoice);
void choosePaymentMethod(const PaymentMethodDetails &method);
void askSetPassword();
bool showWebview(const QString &url, bool allowBack);
@ -81,6 +84,8 @@ private:
const not_null<PanelDelegate*> _delegate;
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<Webview::Window> _webview;
std::unique_ptr<RpWidget> _webviewBottom;
QPointer<Checkbox> _saveWebviewInformation;
QPointer<FormSummary> _weakFormSummary;
QPointer<EditInformation> _weakEditInformation;
QPointer<EditCard> _weakEditCard;

View file

@ -154,6 +154,7 @@ struct NativeMethodDetails {
bool needCountry = false;
bool needZip = false;
bool needCardholderName = false;
bool canSaveInformation = false;
};
struct PaymentMethodDetails {
@ -161,6 +162,7 @@ struct PaymentMethodDetails {
NativeMethodDetails native;
QString url;
bool ready = false;
bool canSaveInformation = false;
};
enum class CardField {

View file

@ -28,8 +28,11 @@ public:
virtual void panelRequestClose() = 0;
virtual void panelCloseSure() = 0;
virtual void panelSubmit() = 0;
virtual void panelWebviewMessage(const QJsonDocument &message) = 0;
virtual void panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) = 0;
virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;
virtual void panelSetPassword() = 0;
virtual void panelCancelEdit() = 0;
virtual void panelEditPaymentMethod() = 0;
@ -43,7 +46,9 @@ public:
virtual void panelChangeTips(int64 value) = 0;
virtual void panelValidateInformation(RequestedInformation data) = 0;
virtual void panelValidateCard(Ui::UncheckedCardDetails data) = 0;
virtual void panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) = 0;
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
virtual QString panelWebviewDataPath() = 0;