diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 37c0c79a40..27e9dda9bd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1555,7 +1555,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_passport_use_existing_email" = "Use the same email as on Telegram."; "lng_passport_new_email" = "Or enter a new email"; "lng_passport_new_email_code" = "Note: You will receive a confirmation code on the email address you provide."; -"lng_passport_email_enter_code" = "Please enter the confirmation code we've just sent to {email}."; +"lng_passport_confirm_phone" = "We've sent an SMS with a confirmation code to your phone {phone}."; +"lng_passport_confirm_email" = "We've sent a confirmation code to your email {email}."; +"lng_passport_sure_cancel" = "If you continue your changes will be lost."; // Wnd specific diff --git a/Telegram/Resources/scheme.tl b/Telegram/Resources/scheme.tl index 74ca2a6074..d09ab90ca3 100644 --- a/Telegram/Resources/scheme.tl +++ b/Telegram/Resources/scheme.tl @@ -999,7 +999,7 @@ secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureC account.authorizationForm#b9d3d1f0 flags:# selfie_required:flags.1?true required_types:Vector values:Vector users:Vector privacy_policy_url:flags.0?string = account.AuthorizationForm; -account.sentEmailCode#28b1633b email_pattern:string = account.SentEmailCode; +account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode; ---functions--- diff --git a/Telegram/SourceFiles/boxes/change_phone_box.cpp b/Telegram/SourceFiles/boxes/change_phone_box.cpp index b6a97e0a3c..87a114be23 100644 --- a/Telegram/SourceFiles/boxes/change_phone_box.cpp +++ b/Telegram/SourceFiles/boxes/change_phone_box.cpp @@ -213,7 +213,7 @@ ChangePhoneBox::EnterCode::EnterCode(QWidget*, const QString &phone, const QStri , _hash(hash) , _codeLength(codeLength) , _callTimeout(callTimeout) -, _call(this, [this] { sendCall(); }, [this] { updateCall(); }) { +, _call([this] { sendCall(); }, [this] { updateCall(); }) { } void ChangePhoneBox::EnterCode::prepare() { diff --git a/Telegram/SourceFiles/boxes/confirm_phone_box.cpp b/Telegram/SourceFiles/boxes/confirm_phone_box.cpp index e3687a3b09..f001e4124f 100644 --- a/Telegram/SourceFiles/boxes/confirm_phone_box.cpp +++ b/Telegram/SourceFiles/boxes/confirm_phone_box.cpp @@ -76,15 +76,14 @@ void SentCodeField::fix() { } } -SentCodeCall::SentCodeCall(QObject *parent, base::lambda_once callCallback, base::lambda updateCallback) -: _timer(parent) -, _call(std::move(callCallback)) +SentCodeCall::SentCodeCall(base::lambda_once callCallback, base::lambda updateCallback) +: _call(std::move(callCallback)) , _update(std::move(updateCallback)) { - _timer->connect(_timer, &QTimer::timeout, [this] { + _timer.setCallback([=] { if (_status.state == State::Waiting) { if (--_status.timeout <= 0) { _status.state = State::Calling; - _timer->stop(); + _timer.cancel(); if (_call) { _call(); } @@ -99,7 +98,7 @@ SentCodeCall::SentCodeCall(QObject *parent, base::lambda_once callCallba void SentCodeCall::setStatus(const Status &status) { _status = status; if (_status.state == State::Waiting) { - _timer->start(1000); + _timer.callEach(1000); } } @@ -130,7 +129,7 @@ void ConfirmPhoneBox::start(const QString &phone, const QString &hash) { ConfirmPhoneBox::ConfirmPhoneBox(QWidget*, const QString &phone, const QString &hash) : _phone(phone) , _hash(hash) -, _call(this, [this] { sendCall(); }, [this] { update(); }) { +, _call([this] { sendCall(); }, [this] { update(); }) { } void ConfirmPhoneBox::sendCall() { diff --git a/Telegram/SourceFiles/boxes/confirm_phone_box.h b/Telegram/SourceFiles/boxes/confirm_phone_box.h index 170f6128f9..bde65d12c4 100644 --- a/Telegram/SourceFiles/boxes/confirm_phone_box.h +++ b/Telegram/SourceFiles/boxes/confirm_phone_box.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "boxes/abstract_box.h" +#include "base/timer.h" #include "ui/widgets/input_fields.h" namespace Ui { @@ -43,7 +44,9 @@ private: class SentCodeCall { public: - SentCodeCall(QObject *parent, base::lambda_once callCallback, base::lambda updateCallback); + SentCodeCall( + base::lambda_once callCallback, + base::lambda updateCallback); enum class State { Waiting, @@ -75,7 +78,7 @@ public: private: Status _status; - object_ptr _timer; + base::Timer _timer; base::lambda_once _call; base::lambda _update; diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index fec484ace9..5f1cf07d1b 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -30,6 +30,9 @@ passportPasswordHintLabel: passportPasswordLabel; passportErrorLabel: FlatLabel(passportPasswordLabel) { textFg: boxTextFgError; } +passportVerifyErrorLabel: FlatLabel(passportErrorLabel) { + align: align(topleft); +} passportPanelWidth: 392px; passportPanelHeight: 600px; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index 5ba68a5177..1b60493b9d 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -451,16 +451,84 @@ auto FormController::scanUpdated() const return _scanUpdated.events(); } -auto FormController::valueSaved() const -->rpl::producer> { - return _valueSaved.events(); +auto FormController::valueSaveFinished() const +-> rpl::producer> { + return _valueSaveFinished.events(); } auto FormController::verificationNeeded() const -->rpl::producer> { +-> rpl::producer> { return _verificationNeeded.events(); } +auto FormController::verificationUpdate() const +-> rpl::producer> { + return _verificationUpdate.events(); +} + +void FormController::verify( + not_null value, + const QString &code) { + if (value->verification.requestId) { + return; + } + const auto nonconst = findValue(value); + const auto prepared = code.trimmed(); + Assert(nonconst->verification.codeLength != 0); + verificationError(nonconst, QString()); + if (nonconst->verification.codeLength > 0 + && nonconst->verification.codeLength != prepared.size()) { + verificationError(nonconst, lang(lng_signin_wrong_code)); + return; + } else if (prepared.isEmpty()) { + verificationError(nonconst, lang(lng_signin_wrong_code)); + return; + } + nonconst->verification.requestId = [&] { + switch (nonconst->type) { + case Value::Type::Phone: + return request(MTPaccount_VerifyPhone( + MTP_string(getPhoneFromValue(nonconst)), + MTP_string(nonconst->verification.phoneCodeHash), + MTP_string(prepared) + )).done([=](const MTPBool &result) { + savePlainTextValue(nonconst); + clearValueVerification(nonconst); + }).fail([=](const RPCError &error) { + nonconst->verification.requestId = 0; + if (error.type() == qstr("PHONE_CODE_INVALID")) { + verificationError(nonconst, lang(lng_signin_wrong_code)); + } else { + verificationError(nonconst, error.type()); + } + }).send(); + case Value::Type::Email: + return request(MTPaccount_VerifyEmail( + MTP_string(getEmailFromValue(nonconst)), + MTP_string(prepared) + )).done([=](const MTPBool &result) { + savePlainTextValue(nonconst); + clearValueVerification(nonconst); + }).fail([=](const RPCError &error) { + nonconst->verification.requestId = 0; + if (error.type() == qstr("CODE_INVALID")) { + verificationError(nonconst, lang(lng_signin_wrong_code)); + } else { + verificationError(nonconst, error.type()); + } + }).send(); + } + Unexpected("Type in FormController::verify()."); + }(); +} + +void FormController::verificationError( + not_null value, + const QString &text) { + value->verification.error = text; + _verificationUpdate.fire_copy(value); +} + const Form &FormController::form() const { return _form; } @@ -475,15 +543,16 @@ not_null FormController::findValue(not_null value) { } void FormController::startValueEdit(not_null value) { - if (value->saveRequestId) { + const auto nonconst = findValue(value); + ++nonconst->editScreens; + if (savingValue(nonconst)) { return; } - const auto nonconst = findValue(value); loadFiles(nonconst->files); nonconst->filesInEdit = ranges::view::all( - value->files + nonconst->files ) | ranges::view::transform([=](const File &file) { - return EditFile(value, file, nullptr); + return EditFile(nonconst, file, nullptr); }) | ranges::to_vector; nonconst->data.parsedInEdit = nonconst->data.parsed; } @@ -570,28 +639,112 @@ void FormController::fileLoadFail(FileKey key) { } } +bool FormController::savingValue(not_null value) const { + return (value->saveRequestId != 0) + || (value->verification.requestId != 0) + || (value->verification.codeLength != 0); +} + void FormController::cancelValueEdit(not_null value) { - if (value->saveRequestId) { + Expects(value->editScreens > 0); + + const auto nonconst = findValue(value); + --nonconst->editScreens; + clearValueEdit(nonconst); +} + +void FormController::valueEditFailed(not_null value) { + Expects(!savingValue(value)); + + if (value->editScreens == 0) { + clearValueEdit(value); + } +} + +void FormController::clearValueEdit(not_null value) { + if (savingValue(value)) { return; } + value->filesInEdit.clear(); + value->data.encryptedSecretInEdit.clear(); + value->data.hashInEdit.clear(); + value->data.parsedInEdit = ValueMap(); +} + +void FormController::cancelValueVerification(not_null value) { const auto nonconst = findValue(value); - nonconst->filesInEdit.clear(); - nonconst->data.encryptedSecretInEdit.clear(); - nonconst->data.hashInEdit.clear(); - nonconst->data.parsedInEdit = ValueMap(); + clearValueVerification(nonconst); + if (!savingValue(nonconst)) { + valueEditFailed(nonconst); + } +} + +void FormController::clearValueVerification(not_null value) { + const auto was = (value->verification.codeLength != 0); + if (const auto requestId = base::take(value->verification.requestId)) { + request(requestId).cancel(); + } + value->verification = Verification(); + if (was) { + _verificationUpdate.fire_copy(value); + } } bool FormController::isEncryptedValue(Value::Type type) const { return (type != Value::Type::Phone && type != Value::Type::Email); } +bool FormController::editFileChanged(const EditFile &file) const { + if (file.uploadData) { + return !file.deleted; + } + return file.deleted; +} + +bool FormController::editValueChanged( + not_null value, + const ValueMap &data) const { + auto filesCount = 0; + for (const auto &file : value->filesInEdit) { + if (editFileChanged(file)) { + return true; + } + } + if (value->selfieInEdit && editFileChanged(*value->selfieInEdit)) { + return true; + } + auto existing = value->data.parsed.fields; + for (const auto &[key, value] : data.fields) { + const auto i = existing.find(key); + if (i != existing.end()) { + if (i->second != value) { + return true; + } + existing.erase(i); + } else if (!value.isEmpty()) { + return true; + } + } + return !existing.empty(); +} + void FormController::saveValueEdit( not_null value, ValueMap &&data) { - if (value->saveRequestId) { + if (savingValue(value)) { return; } + const auto nonconst = findValue(value); + if (!editValueChanged(nonconst, data)) { + base::take(nonconst->filesInEdit); + base::take(nonconst->selfieInEdit); + base::take(nonconst->data.encryptedSecretInEdit); + base::take(nonconst->data.hashInEdit); + base::take(nonconst->data.parsedInEdit); + _valueSaveFinished.fire_copy(nonconst); + return; + } nonconst->data.parsedInEdit = std::move(data); if (isEncryptedValue(nonconst->type)) { @@ -699,7 +852,7 @@ void FormController::saveEncryptedValue(not_null value) { void FormController::savePlainTextValue(not_null value) { Expects(!isEncryptedValue(value->type)); - const auto text = value->data.parsedInEdit.fields["value"]; + const auto text = getPlainTextFromValue(value); const auto type = [&] { switch (value->type) { case Value::Type::Phone: return MTP_secureValueTypePhone(); @@ -734,6 +887,8 @@ void FormController::sendSaveRequest( )).done([=](const MTPSecureValue &result) { Expects(result.type() == mtpc_secureValue); + value->saveRequestId = 0; + const auto &data = result.c_secureValue(); value->files = parseFiles( data.vfiles.v, @@ -743,26 +898,146 @@ void FormController::sendSaveRequest( value->data.parsed = std::move(value->data.parsedInEdit); value->data.hash = std::move(value->data.hashInEdit); - value->saveRequestId = 0; - - _valueSaved.fire_copy(value); + _valueSaveFinished.fire_copy(value); }).fail([=](const RPCError &error) { value->saveRequestId = 0; if (error.type() == qstr("PHONE_VERIFICATION_NEEDED")) { if (value->type == Value::Type::Phone) { - _verificationNeeded.fire_copy(value); + startPhoneVerification(value); return; } } else if (error.type() == qstr("EMAIL_VERIFICATION_NEEDED")) { if (value->type == Value::Type::Email) { - _verificationNeeded.fire_copy(value); + startEmailVerification(value); return; } } - _view->show(Box("Error saving value:\n" + error.type())); + valueSaveFailed(value, error); }).send(); } +QString FormController::getPhoneFromValue( + not_null value) const { + Expects(value->type == Value::Type::Phone); + + return getPlainTextFromValue(value); +} + +QString FormController::getEmailFromValue( + not_null value) const { + Expects(value->type == Value::Type::Email); + + return getPlainTextFromValue(value); +} + +QString FormController::getPlainTextFromValue( + not_null value) const { + Expects(value->type == Value::Type::Phone + || value->type == Value::Type::Email); + + const auto i = value->data.parsedInEdit.fields.find("value"); + Assert(i != end(value->data.parsedInEdit.fields)); + return i->second; +} + +void FormController::startPhoneVerification(not_null value) { + value->verification.requestId = request(MTPaccount_SendVerifyPhoneCode( + MTP_flags(MTPaccount_SendVerifyPhoneCode::Flag(0)), + MTP_string(getPhoneFromValue(value)), + MTPBool() + )).done([=](const MTPauth_SentCode &result) { + Expects(result.type() == mtpc_auth_sentCode); + + value->verification.requestId = 0; + + const auto &data = result.c_auth_sentCode(); + value->verification.phoneCodeHash = qs(data.vphone_code_hash); + switch (data.vtype.type()) { + case mtpc_auth_sentCodeTypeApp: + LOG(("API Error: sentCodeTypeApp not expected " + "in FormController::startPhoneVerification.")); + return; + case mtpc_auth_sentCodeTypeFlashCall: + LOG(("API Error: sentCodeTypeFlashCall not expected " + "in FormController::startPhoneVerification.")); + return; + case mtpc_auth_sentCodeTypeCall: { + const auto &type = data.vtype.c_auth_sentCodeTypeCall(); + value->verification.codeLength = (type.vlength.v > 0) + ? type.vlength.v + : -1; + value->verification.call = std::make_unique( + [=] { requestPhoneCall(value); }, + [=] { _verificationUpdate.fire_copy(value); }); + value->verification.call->setStatus( + { SentCodeCall::State::Called, 0 }); + if (data.has_next_type()) { + LOG(("API Error: next_type is not supported for calls.")); + } + } break; + case mtpc_auth_sentCodeTypeSms: { + const auto &type = data.vtype.c_auth_sentCodeTypeSms(); + value->verification.codeLength = (type.vlength.v > 0) + ? type.vlength.v + : -1; + const auto &next = data.vnext_type; + if (data.has_next_type() + && next.type() == mtpc_auth_codeTypeCall) { + value->verification.call = std::make_unique( + [=] { requestPhoneCall(value); }, + [=] { _verificationUpdate.fire_copy(value); }); + value->verification.call->setStatus({ + SentCodeCall::State::Waiting, + data.has_timeout() ? data.vtimeout.v : 60 }); + } + } break; + } + _verificationNeeded.fire_copy(value); + }).fail([=](const RPCError &error) { + value->verification.requestId = 0; + valueSaveFailed(value, error); + }).send(); +} + +void FormController::startEmailVerification(not_null value) { + value->verification.requestId = request(MTPaccount_SendVerifyEmailCode( + MTP_string(getEmailFromValue(value)) + )).done([=](const MTPaccount_SentEmailCode &result) { + Expects(result.type() == mtpc_account_sentEmailCode); + + value->verification.requestId = 0; + const auto &data = result.c_account_sentEmailCode(); + value->verification.codeLength = (data.vlength.v > 0) + ? data.vlength.v + : -1; + _verificationNeeded.fire_copy(value); + }).fail([=](const RPCError &error) { + valueSaveFailed(value, error); + }).send(); +} + + +void FormController::requestPhoneCall(not_null value) { + Expects(value->verification.call != nullptr); + + value->verification.call->setStatus( + { SentCodeCall::State::Calling, 0 }); + request(MTPauth_ResendCode( + MTP_string(getPhoneFromValue(value)), + MTP_string(value->verification.phoneCodeHash) + )).done([=](const MTPauth_SentCode &code) { + value->verification.call->callDone(); + }).send(); +} + +void FormController::valueSaveFailed( + not_null value, + const RPCError &error) { + _view->show(Box("Error saving value:\n" + error.type())); + valueEditFailed(value); + _valueSaveFinished.fire_copy(value); +} + void FormController::generateSecret(bytes::const_span password) { if (_saveSecretRequestId) { return; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index 103e097427..cca4fa2ec9 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "mtproto/sender.h" +#include "boxes/confirm_phone_box.h" #include "base/weak_ptr.h" class BoxContent; @@ -111,6 +112,16 @@ struct ValueData { bytes::vector encryptedSecretInEdit; }; +struct Verification { + mtpRequestId requestId = 0; + QString phoneCodeHash; + int codeLength = 0; + std::unique_ptr call; + + QString error; + +}; + struct Value { enum class Type { PersonalDetails, @@ -135,6 +146,9 @@ struct Value { std::vector filesInEdit; base::optional selfie; base::optional selfieInEdit; + Verification verification; + + int editScreens = 0; mtpRequestId saveRequestId = 0; }; @@ -205,13 +219,20 @@ public: QString defaultPhoneNumber() const; rpl::producer> scanUpdated() const; - rpl::producer> valueSaved() const; + rpl::producer> valueSaveFinished() const; rpl::producer> verificationNeeded() const; + rpl::producer> verificationUpdate() const; + void verify(not_null value, const QString &code); const Form &form() const; void startValueEdit(not_null value); void cancelValueEdit(not_null value); + void cancelValueVerification(not_null value); + bool editValueChanged( + not_null value, + const ValueMap &data) const; void saveValueEdit(not_null value, ValueMap &&data); + bool savingValue(not_null value) const; void cancel(); @@ -274,6 +295,21 @@ private: void scanUploadFail(const FullMsgId &fullId); void scanDeleteRestore(not_null value, int fileIndex, bool deleted); + QString getPhoneFromValue(not_null value) const; + QString getEmailFromValue(not_null value) const; + QString getPlainTextFromValue(not_null value) const; + void startPhoneVerification(not_null value); + void startEmailVerification(not_null value); + void valueSaveFailed(not_null value, const RPCError &error); + void requestPhoneCall(not_null value); + void verificationError( + not_null value, + const QString &text); + void valueEditFailed(not_null value); + void clearValueEdit(not_null value); + void clearValueVerification(not_null value); + bool editFileChanged(const EditFile &file) const; + bool isEncryptedValue(Value::Type type) const; void saveEncryptedValue(not_null value); void savePlainTextValue(not_null value); @@ -295,8 +331,9 @@ private: std::map> _fileLoaders; rpl::event_stream> _scanUpdated; - rpl::event_stream> _valueSaved; + rpl::event_stream> _valueSaveFinished; rpl::event_stream> _verificationNeeded; + rpl::event_stream> _verificationUpdate; bytes::vector _secret; uint64 _secretId = 0; diff --git a/Telegram/SourceFiles/passport/passport_panel.cpp b/Telegram/SourceFiles/passport/passport_panel.cpp index bf919c5499..a210b4d157 100644 --- a/Telegram/SourceFiles/passport/passport_panel.cpp +++ b/Telegram/SourceFiles/passport/passport_panel.cpp @@ -83,7 +83,9 @@ void Panel::updateTitlePosition() { } rpl::producer<> Panel::backRequests() const { - return _back->entity()->clicks(); + return rpl::merge( + _back->entity()->clicks(), + _synteticBackRequests.events()); } void Panel::setBackAllowed(bool allowed) { @@ -100,10 +102,11 @@ void Panel::showAndActivate() { setFocus(); } -bool Panel::eventHook(QEvent *e) { - if (e->type() == QEvent::WindowDeactivate) { +void Panel::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape && _back->toggled()) { + _synteticBackRequests.fire({}); } - return RpWidget::eventHook(e); + return RpWidget::keyPressEvent(e); } void Panel::initLayout() { @@ -276,6 +279,10 @@ void Panel::showInner(base::unique_qptr inner) { }, _inner->lifetime()); _inner->show(); + if (_layer) { + _layer->raise(); + } + showAndActivate(); } diff --git a/Telegram/SourceFiles/passport/passport_panel.h b/Telegram/SourceFiles/passport/passport_panel.h index 0f10b02bfc..fb547d3fb8 100644 --- a/Telegram/SourceFiles/passport/passport_panel.h +++ b/Telegram/SourceFiles/passport/passport_panel.h @@ -55,7 +55,7 @@ protected: void mouseMoveEvent(QMouseEvent *e) override; void leaveEventHook(QEvent *e) override; void leaveToChildEvent(QEvent *e, QWidget *child) override; - bool eventHook(QEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; private: void initControls(); @@ -83,6 +83,7 @@ private: object_ptr _body; base::unique_qptr _inner; object_ptr _layer = { nullptr }; + rpl::event_stream<> _synteticBackRequests; bool _useTransparency = true; style::margins _padding; diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index e9eded87f2..1d06c106f6 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -225,6 +225,19 @@ PanelController::PanelController(not_null form) _panel->showForm(); } }, lifetime()); + + _form->verificationNeeded( + ) | rpl::start_with_next([=](not_null value) { + processVerificationNeeded(value); + }, lifetime()); + + _form->verificationUpdate( + ) | rpl::filter([=](not_null field) { + return (field->verification.codeLength == 0); + }) | rpl::start_with_next([=](not_null field) { + _verificationBoxes.erase(field); + }, lifetime()); + _scopes = ComputeScopes(_form); } @@ -417,8 +430,8 @@ void PanelController::editScope(int index) { auto content = [&]() -> object_ptr { switch (_editScope->type) { case Scope::Type::Identity: - case Scope::Type::Address: - return (_editScopeFilesIndex >= 0) + case Scope::Type::Address: { + auto result = (_editScopeFilesIndex >= 0) ? object_ptr( _panel.get(), this, @@ -431,10 +444,17 @@ void PanelController::editScope(int index) { this, std::move(GetDocumentScheme(_editScope->type)), _editScope->fields->data.parsedInEdit); + const auto weak = make_weak(result.data()); + _panelHasUnsavedChanges = [=] { + return weak ? weak->hasUnsavedChanges() : false; + }; + return std::move(result); + } break; case Scope::Type::Phone: case Scope::Type::Email: { const auto &parsed = _editScope->fields->data.parsedInEdit; const auto valueIt = parsed.fields.find("value"); + _panelHasUnsavedChanges = nullptr; return object_ptr( _panel.get(), this, @@ -448,48 +468,106 @@ void PanelController::editScope(int index) { Unexpected("Type in PanelController::editScope()."); }(); - _panel->setBackAllowed(true); - - _panel->backRequests( - ) | rpl::start_with_next([=] { - _panel->showForm(); - }, content->lifetime()); - content->lifetime().add([=] { cancelValueEdit(); }); - _form->valueSaved( - ) | rpl::start_with_next([=](not_null value) { - processValueSaved(value); + _panel->setBackAllowed(true); + + _panel->backRequests( + ) | rpl::start_with_next([=] { + cancelEditScope(); }, content->lifetime()); - _form->verificationNeeded( + _form->valueSaveFinished( ) | rpl::start_with_next([=](not_null value) { - processVerificationNeeded(value); + processValueSaveFinished(value); }, content->lifetime()); _panel->showEditValue(std::move(content)); } -void PanelController::processValueSaved(not_null value) { +void PanelController::processValueSaveFinished( + not_null value) { Expects(_editScope != nullptr); + const auto boxIt = _verificationBoxes.find(value); + if (boxIt != end(_verificationBoxes)) { + const auto saved = std::move(boxIt->second); + _verificationBoxes.erase(boxIt); + } + const auto value1 = _editScope->fields; const auto value2 = (_editScopeFilesIndex >= 0) ? _editScope->files[_editScopeFilesIndex].get() : nullptr; if (value == value1 || value == value2) { - if (!value1->saveRequestId && (!value2 || !value2->saveRequestId)) { + if (!_form->savingValue(value1) + && (!value2 || !_form->savingValue(value2))) { _panel->showForm(); - show(Box("Saved")); } } } void PanelController::processVerificationNeeded( not_null value) { - show(Box("Verification needed :(")); + const auto i = _verificationBoxes.find(value); + if (i != _verificationBoxes.end()) { + LOG(("API Error: Requesting for verification repeatedly.")); + return; + } + const auto textIt = value->data.parsedInEdit.fields.find("value"); + Assert(textIt != end(value->data.parsedInEdit.fields)); + const auto text = textIt->second; + const auto type = value->type; + const auto update = _form->verificationUpdate( + ) | rpl::filter([=](not_null field) { + return (field == value); + }); + const auto box = [&] { + if (type == Value::Type::Phone) { + return show(VerifyPhoneBox( + text, + value->verification.codeLength, + [=](const QString &code) { _form->verify(value, code); }, + + value->verification.call ? rpl::single( + value->verification.call->getText() + ) | rpl::then(rpl::duplicate( + update + ) | rpl::filter([=](not_null field) { + return field->verification.call != nullptr; + }) | rpl::map([=](not_null field) { + return field->verification.call->getText(); + })) : (rpl::single(QString()) | rpl::type_erased()), + + rpl::duplicate( + update + ) | rpl::map([=](not_null field) { + return field->verification.error; + }) | rpl::distinct_until_changed())); + } else if (type == Value::Type::Email) { + return show(VerifyEmailBox( + text, + value->verification.codeLength, + [=](const QString &code) { _form->verify(value, code); }, + + rpl::duplicate( + update + ) | rpl::map([=](not_null field) { + return field->verification.error; + }) | rpl::distinct_until_changed())); + } else { + Unexpected("Type in processVerificationNeeded."); + } + }(); + + box->boxClosing( + ) | rpl::start_with_next([=] { + _form->cancelValueVerification(value); + }, lifetime()); + + _verificationBoxes.emplace(value, box); } std::vector PanelController::valueFiles( @@ -525,6 +603,36 @@ void PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) { } } +bool PanelController::editScopeChanged( + const ValueMap &data, + const ValueMap &filesData) const { + Expects(_editScope != nullptr); + + if (_form->editValueChanged(_editScope->fields, data)) { + return true; + } else if (_editScopeFilesIndex >= 0) { + return _form->editValueChanged( + _editScope->files[_editScopeFilesIndex], + filesData); + } + return false; +} + +void PanelController::cancelEditScope() { + Expects(_editScope != nullptr); + + if (_panelHasUnsavedChanges && _panelHasUnsavedChanges()) { + if (!_confirmForgetChangesBox) { + _confirmForgetChangesBox = BoxPointer(show(Box( + lang(lng_passport_sure_cancel), + lang(lng_continue), + [=] { _panel->showForm(); })).data()); + } + } else { + _panel->showForm(); + } +} + void PanelController::cancelAuth() { _form->cancel(); } diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h index fd111feca8..995fd460da 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.h +++ b/Telegram/SourceFiles/passport/passport_panel_controller.h @@ -71,6 +71,10 @@ public: void editScope(int index) override; void saveScope(ValueMap &&data, ValueMap &&filesData); + bool editScopeChanged( + const ValueMap &data, + const ValueMap &filesData) const; + void cancelEditScope(); void showBox(object_ptr box) override; @@ -83,7 +87,7 @@ private: void cancelValueEdit(); std::vector valueFiles(const Value &value) const; - void processValueSaved(not_null value); + void processValueSaveFinished(not_null value); void processVerificationNeeded(not_null value); ScanInfo collectScanInfo(const EditFile &file) const; @@ -93,8 +97,11 @@ private: std::vector _scopes; std::unique_ptr _panel; + base::lambda _panelHasUnsavedChanges; + BoxPointer _confirmForgetChangesBox; Scope *_editScope = nullptr; int _editScopeFilesIndex = -1; + std::map, BoxPointer> _verificationBoxes; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp index 2603ae2a66..7977ffeb6d 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp @@ -16,11 +16,143 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/wrap/vertical_layout.h" +#include "ui/wrap/fade_wrap.h" #include "boxes/abstract_box.h" +#include "boxes/confirm_phone_box.h" #include "lang/lang_keys.h" #include "styles/style_passport.h" +#include "styles/style_boxes.h" namespace Passport { +namespace { + +class VerifyBox : public BoxContent { +public: + VerifyBox( + QWidget *parent, + const QString &title, + const QString &text, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error); + + void setInnerFocus() override; + +protected: + void prepare() override; + +private: + void setupControls( + const QString &text, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error); + + QString _title; + base::lambda _submit; + QPointer _code; + int _height = 0; + +}; + +VerifyBox::VerifyBox( + QWidget *parent, + const QString &title, + const QString &text, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error) +: _title(title) { + setupControls(text, codeLength, submit, std::move(call), std::move(error)); +} + +void VerifyBox::setupControls( + const QString &text, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error) { + const auto description = Ui::CreateChild( + this, + text, + Ui::FlatLabel::InitType::Simple, + st::boxLabel); + _code = Ui::CreateChild( + this, + st::defaultInputField, + langFactory(lng_change_phone_code_title)); + + const auto problem = Ui::CreateChild>( + this, + object_ptr( + this, + QString(), + Ui::FlatLabel::InitType::Simple, + st::passportVerifyErrorLabel)); + const auto waiter = Ui::CreateChild( + this, + std::move(call), + st::passportFormLabel); + std::move( + error + ) | rpl::start_with_next([=](const QString &error) { + if (error.isEmpty()) { + problem->hide(anim::type::normal); + } else { + problem->entity()->setText(error); + problem->show(anim::type::normal); + _code->showError(); + } + }, lifetime()); + + auto y = 0; + const auto innerWidth = st::boxWidth + - st::boxPadding.left() + - st::boxPadding.right(); + description->resizeToWidth(innerWidth); + description->moveToLeft(st::boxPadding.left(), y); + y += description->height() + st::boxPadding.bottom(); + _code->resizeToWidth(innerWidth); + _code->moveToLeft(st::boxPadding.left(), y); + y += _code->height() + st::boxPadding.bottom(); + problem->resizeToWidth(innerWidth); + problem->moveToLeft(st::boxPadding.left(), y); + y += problem->height() + st::boxPadding.top(); + waiter->resizeToWidth(innerWidth); + waiter->moveToLeft(st::boxPadding.left(), y); + y += waiter->height() + st::boxPadding.bottom(); + + _submit = [=] { + submit(_code->getLastText()); + }; + if (codeLength > 0) { + _code->setAutoSubmit(codeLength, _submit); + } else { + connect(_code, &SentCodeField::submitted, _submit); + } + connect(_code, &SentCodeField::changed, [=] { + problem->hide(anim::type::normal); + }); + _height = y; +} + +void VerifyBox::setInnerFocus() { + _code->setFocusFast(); +} + +void VerifyBox::prepare() { + setTitle([=] { return _title; }); + + addButton(langFactory(lng_change_phone_new_submit), _submit); + addButton(langFactory(lng_cancel), [=] { closeBox(); }); + + setDimensions(st::boxWidth, _height); +} + +} // namespace PanelEditContact::PanelEditContact( QWidget*, @@ -87,14 +219,17 @@ void PanelEditContact::setupControls( _field = _content->add( object_ptr( _content, - st::passportDetailsField), + st::passportDetailsField, + nullptr, + data), st::passportContactNewFieldPadding); } else { _field = _content->add( object_ptr( _content, st::passportContactField, - _scheme.newPlaceholder), + _scheme.newPlaceholder, + data), st::passportContactFieldPadding); } _content->add( @@ -107,11 +242,13 @@ void PanelEditContact::setupControls( st::passportFormLabel), st::passportFormLabelPadding)); - _done->addClickHandler([=] { + const auto submit = [=] { crl::on_main(this, [=] { save(); }); - }); + }; + connect(_field, &Ui::InputField::submitted, submit); + _done->addClickHandler(submit); } void PanelEditContact::focusInEvent(QFocusEvent *e) { @@ -148,4 +285,33 @@ void PanelEditContact::save(const QString &value) { _controller->saveScope(std::move(data), {}); } +object_ptr VerifyPhoneBox( + const QString &phone, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error) { + return Box( + lang(lng_passport_phone_title), + lng_passport_confirm_phone(lt_phone, App::formatPhone(phone)), + codeLength, + submit, + std::move(call), + std::move(error)); +} + +object_ptr VerifyEmailBox( + const QString &email, + int codeLength, + base::lambda submit, + rpl::producer error) { + return Box( + lang(lng_passport_email_title), + lng_passport_confirm_email(lt_email, email), + codeLength, + submit, + rpl::single(QString()), + std::move(error)); +} + } // namespace Passport diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.h b/Telegram/SourceFiles/passport/passport_panel_edit_contact.h index 5935df90dc..364f8733fa 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.h @@ -69,4 +69,16 @@ private: }; +object_ptr VerifyPhoneBox( + const QString &phone, + int codeLength, + base::lambda submit, + rpl::producer call, + rpl::producer error); +object_ptr VerifyEmailBox( + const QString &email, + int codeLength, + base::lambda submit, + rpl::producer error); + } // namespace Passport diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index 7111459c66..0ff97dd874 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "boxes/abstract_box.h" +#include "boxes/confirm_box.h" #include "lang/lang_keys.h" #include "styles/style_widgets.h" #include "styles/style_boxes.h" @@ -25,6 +26,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Passport { +struct PanelEditDocument::Result { + ValueMap data; + ValueMap filesData; +}; + PanelEditDocument::PanelEditDocument( QWidget*, not_null controller, @@ -144,6 +150,11 @@ void PanelEditDocument::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } +bool PanelEditDocument::hasUnsavedChanges() const { + const auto result = collect(); + return _controller->editScopeChanged(result.data, result.filesData); +} + void PanelEditDocument::updateControlsGeometry() { const auto submitTop = height() - _done->height(); _scroll->setGeometry(0, 0, width(), submitTop); @@ -157,17 +168,23 @@ void PanelEditDocument::updateControlsGeometry() { _scroll->updateBars(); } -void PanelEditDocument::save() { - auto data = ValueMap(); - auto scanData = ValueMap(); +PanelEditDocument::Result PanelEditDocument::collect() const { + auto result = Result(); for (const auto [i, field] : _details) { const auto &row = _scheme.rows[i]; auto &fields = (row.type == Scheme::ValueType::Fields) - ? data - : scanData; - fields.fields[row.key] = _details[i]->getValue(); + ? result.data + : result.filesData; + fields.fields[row.key] = field->getValue(); } - _controller->saveScope(std::move(data), std::move(scanData)); + return result; +} + +void PanelEditDocument::save() { + auto result = collect(); + _controller->saveScope( + std::move(result.data), + std::move(result.filesData)); } } // namespace Passport diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h index aadeca6198..8a598da5ed 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h @@ -56,11 +56,14 @@ public: Scheme scheme, const ValueMap &data); + bool hasUnsavedChanges() const; + protected: void focusInEvent(QFocusEvent *e) override; void resizeEvent(QResizeEvent *e) override; private: + struct Result; void setupControls( const ValueMap &data, const ValueMap *scanData, @@ -71,6 +74,7 @@ private: std::vector &&files); void updateControlsGeometry(); + Result collect() const; void save(); not_null _controller;