From 663db64688cf5d6dddd6c6525b7d7467c5a6bfd7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 Mar 2021 21:15:49 +0400 Subject: [PATCH] Allow saving and using saved credentials. --- Telegram/Resources/langs/lang.strings | 6 +- Telegram/SourceFiles/boxes/passcode_box.cpp | 28 ++-- Telegram/SourceFiles/boxes/passcode_box.h | 2 + Telegram/SourceFiles/main/main_session.cpp | 16 +++ Telegram/SourceFiles/main/main_session.h | 6 + .../SourceFiles/mtproto/mtproto_response.h | 6 +- .../payments/payments_checkout_process.cpp | 134 +++++++++++++++--- .../payments/payments_checkout_process.h | 28 +++- .../SourceFiles/payments/payments_form.cpp | 82 +++++++++-- Telegram/SourceFiles/payments/payments_form.h | 21 ++- .../payments/ui/payments_edit_card.cpp | 11 +- .../payments/ui/payments_edit_card.h | 2 + .../payments/ui/payments_panel.cpp | 47 +++++- .../SourceFiles/payments/ui/payments_panel.h | 5 + .../payments/ui/payments_panel_data.h | 2 + .../payments/ui/payments_panel_delegate.h | 9 +- 16 files changed, 350 insertions(+), 55 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 00d88d43c..7b7241944 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index bf664d060..83ac3f351 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -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; diff --git a/Telegram/SourceFiles/boxes/passcode_box.h b/Telegram/SourceFiles/boxes/passcode_box.h index 80644c2be..875bed383 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.h +++ b/Telegram/SourceFiles/boxes/passcode_box.h @@ -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, diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index fcb1b1064..ddcb994d6 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -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 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(); diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 712e86397..cd29bcb5b 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -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> _windows; base::Timer _saveSettingsTimer; + QByteArray _tmpPassword; + TimeId _tmpPasswordValidUntil = 0; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/mtproto/mtproto_response.h b/Telegram/SourceFiles/mtproto/mtproto_response.h index 784e9b5e2..247f1c7db 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_response.h +++ b/Telegram/SourceFiles/mtproto/mtproto_response.h @@ -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) { diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 376e3b36d..ba91af348 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -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 @@ -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(_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( + _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 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); } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 06ca7ca7e..26d207d85 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -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 old); + void requestPassword(); + void getPasswordState( + Fn 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 box) override; QString panelWebviewDataPath() override; @@ -105,10 +125,12 @@ private: const not_null _session; const std::unique_ptr
_form; const std::unique_ptr _panel; + QPointer _enterPasswordBox; Fn _reactivate; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; + rpl::lifetime _gettingPasswordState; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 63597878e..c1b001af1 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -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 @@ -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(&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) { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index ae37feece..2edf2acb7 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -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; diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 4abdf760d..d084cc1bd 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -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 EditCard::setupContent() { }, lifetime()); } } + if (_native.canSaveInformation) { + _save = inner->add( + object_ptr( + inner, + tr::lng_payments_save_information(tr::now), + false), + st::paymentsSaveCheckboxPadding); + } return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.h b/Telegram/SourceFiles/payments/ui/payments_edit_card.h index da6a23c4c..ae4cce54b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.h +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.h @@ -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 _name; std::unique_ptr _country; std::unique_ptr _zip; + Checkbox *_save = nullptr; CardField _focusField = CardField::Number; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 48a19dea1..cf74d02cc 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -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( + _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(_widget.get()); - container->setGeometry(_widget->innerGeometry()); + _webviewBottom = std::make_unique(_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( @@ -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 box) { + box->addRow( + object_ptr( + 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) { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 6d39159d3..488419a54 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -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 _delegate; std::unique_ptr _widget; std::unique_ptr _webview; + std::unique_ptr _webviewBottom; + QPointer _saveWebviewInformation; QPointer _weakFormSummary; QPointer _weakEditInformation; QPointer _weakEditCard; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index ce17f7c61..f833703c0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -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 { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index cce968fe1..13a9a86c6 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -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 box) = 0; virtual QString panelWebviewDataPath() = 0;