From 03b7a3ca2ba91aa24d47268c58ea713cf911d461 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 15 Apr 2018 19:06:53 +0400 Subject: [PATCH] Handle errors when saving passport values. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/lang/lang_hardcoded.h | 24 +++ Telegram/SourceFiles/passport/passport.style | 6 +- .../passport/passport_form_controller.cpp | 195 +++++++++++++----- .../passport/passport_form_controller.h | 23 ++- .../passport_form_view_controller.cpp | 31 +-- .../passport/passport_form_view_controller.h | 1 + .../SourceFiles/passport/passport_panel.cpp | 20 +- .../SourceFiles/passport/passport_panel.h | 1 + .../passport/passport_panel_controller.cpp | 62 +++++- .../passport/passport_panel_controller.h | 16 ++ .../passport/passport_panel_edit_contact.cpp | 27 ++- .../passport/passport_panel_edit_document.cpp | 9 +- 13 files changed, 329 insertions(+), 88 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 86e8725de..90be90624 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1604,6 +1604,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_passport_success" = "Authorization successfull!"; "lng_passport_stop_sure" = "Are you sure you want to stop this authorization?"; "lng_passport_stop" = "Stop"; +"lng_passport_restart_sure" = "Unexpected error has occurred. Perhaps some changes were done from a different Telegram application. Would you like to restart this authorization?"; +"lng_passport_restart" = "Restart"; // Wnd specific diff --git a/Telegram/SourceFiles/lang/lang_hardcoded.h b/Telegram/SourceFiles/lang/lang_hardcoded.h index 1cfc93df7..14249bc4a 100644 --- a/Telegram/SourceFiles/lang/lang_hardcoded.h +++ b/Telegram/SourceFiles/lang/lang_hardcoded.h @@ -30,5 +30,29 @@ inline QString ProxyConfigError() { return qsl("The proxy you are using is not configured correctly and will be disabled. Please find another one."); } +inline QString NoAuthorizationBot() { + return qsl("Could not get authorization bot."); +} + +inline QString SecureSaveError() { + return qsl("Error saving value."); +} + +inline QString SecureAcceptError() { + return qsl("Error acception form."); +} + +inline QString PassportCorrupted() { + return qsl("It seems your Telegram Passport data was corrupted.\n\nYou can reset your Telegram Passport and restart this authorization."); +} + +inline QString PassportCorruptedReset() { + return qsl("Reset"); +} + +inline QString PassportCorruptedResetSure() { + return qsl("Are you sure you want to reset your Telegram Passport data?"); +} + } // namespace Hard } // namespace Lang diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index 84b82c11f..627c0b1d4 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -137,8 +137,10 @@ passportFormPolicy: FlatLabel(passportFormLabel) { } } passportFormPolicyPadding: margins(22px, 7px, 22px, 28px); -passportContactNewFieldPadding: margins(22px, 0px, 22px, 28px); -passportContactFieldPadding: margins(22px, 14px, 22px, 28px); +passportContactNewFieldPadding: margins(22px, 0px, 22px, 14px); +passportContactFieldPadding: margins(22px, 14px, 22px, 14px); +passportContactErrorPadding: margins(22px, 0px, 22px, 0px); +passportContactErrorMargin: margins(0px, 0px, 0px, 14px); passportRowPadding: margins(22px, 8px, 25px, 8px); passportRowIconSkip: 10px; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index f02731e16..b2ba564cf 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_controller.h" #include "boxes/confirm_box.h" #include "lang/lang_keys.h" +#include "lang/lang_hardcoded.h" #include "base/openssl_help.h" #include "base/qthelp_url.h" #include "mainwindow.h" @@ -28,6 +29,35 @@ namespace { constexpr auto kDocumentScansLimit = 20; +bool ForwardServiceErrorRequired(const QString &error) { + return (error == qstr("BOT_INVALID")) + || (error == qstr("PUBLIC_KEY_REQUIRED")) + || (error == qstr("PUBLIC_KEY_INVALID")) + || (error == qstr("SCOPE_EMPTY")) + || (error == qstr("PAYLOAD_EMPTY")); +} + +bool SaveErrorRequiresRestart(const QString &error) { + return (error == qstr("PASSWORD_REQUIRED")) + || (error == qstr("SECURE_SECRET_REQUIRED")) + || (error == qstr("SECURE_SECRET_INVALID")); +} + +bool AcceptErrorRequiresRestart(const QString &error) { + return (error == qstr("PASSWORD_REQUIRED")) + || (error == qstr("SECURE_SECRET_REQUIRED")) + || (error == qstr("SECURE_VALUE_EMPTY")) + || (error == qstr("SECURE_VALUE_HASH_INVALID")); +} + +std::map GetTexts(const ValueMap &map) { + auto result = std::map(); + for (const auto &[key, value] : map.fields) { + result[key] = value.text; + } + return result; +} + QImage ReadImage(bytes::const_span buffer) { return App::readImage(QByteArray::fromRawData( reinterpret_cast(buffer.data()), @@ -137,15 +167,15 @@ EditFile::EditFile( not_null value, const File &fields, std::unique_ptr &&uploadData) - : value(value) - , fields(std::move(fields)) - , uploadData(std::move(uploadData)) - , guard(std::make_shared(true)) { +: value(value) +, fields(std::move(fields)) +, uploadData(std::move(uploadData)) +, guard(std::make_shared(true)) { } UploadScanDataPointer::UploadScanDataPointer( std::unique_ptr &&value) - : _value(std::move(value)) { +: _value(std::move(value)) { } UploadScanDataPointer::UploadScanDataPointer( @@ -211,6 +241,7 @@ bytes::vector FormController::passwordHashForAuth( } auto FormController::prepareFinalData() -> FinalData { + auto errors = std::vector>(); auto hashes = QVector(); auto secureData = QJsonObject(); const auto addValueToJSON = [&]( @@ -244,13 +275,11 @@ auto FormController::prepareFinalData() -> FinalData { addValueToJSON(key, value); } }; - auto hasErrors = false; const auto scopes = ComputeScopes(this); for (const auto &scope : scopes) { const auto ready = ComputeScopeRowReadyString(scope); if (ready.isEmpty()) { - hasErrors = true; - findValue(scope.fields)->error = QString(); + errors.push_back(scope.fields); continue; } addValue(scope.fields); @@ -263,28 +292,28 @@ auto FormController::prepareFinalData() -> FinalData { } } } - if (hasErrors) { - return {}; - } auto json = QJsonObject(); - json.insert("secure_data", secureData); - json.insert("payload", _request.payload); + if (errors.empty()) { + json.insert("secure_data", secureData); + json.insert("payload", _request.payload); + } return { hashes, - QJsonDocument(json).toJson(QJsonDocument::Compact) + QJsonDocument(json).toJson(QJsonDocument::Compact), + errors }; } -bool FormController::submit() { +std::vector> FormController::submitGetErrors() { if (_submitRequestId || _submitSuccess|| _cancelled) { - return true; + return {}; } const auto prepared = prepareFinalData(); - if (prepared.hashes.empty()) { - return false; + if (!prepared.errors.empty()) { + return prepared.errors; } const auto credentialsEncryptedData = EncryptData( bytes::make_span(prepared.credentials)); @@ -313,10 +342,15 @@ bool FormController::submit() { [=] { cancel(); }); }).fail([=](const RPCError &error) { _submitRequestId = 0; - _view->show(Box( - "Failed sending data :(\n" + error.type())); + if (AcceptErrorRequiresRestart(error.type())) { + suggestRestart(); + } else { + _view->show(Box( + Lang::Hard::SecureAcceptError() + "\n" + error.type())); + } }).send(); - return true; + + return {}; } void FormController::submitPassword(const QString &password) { @@ -407,10 +441,14 @@ void FormController::decryptValue(Value &value) { return; } if (!value.data.original.isEmpty()) { - value.data.parsed.fields = DeserializeData(DecryptData( + const auto fields = DeserializeData(DecryptData( bytes::make_span(value.data.original), value.data.hash, value.data.secret)); + value.data.parsed.fields.clear(); + for (const auto [key, text] : fields) { + value.data.parsed.fields[key] = { text }; + } } } @@ -991,11 +1029,11 @@ bool FormController::editValueChanged( for (const auto &[key, value] : data.fields) { const auto i = existing.find(key); if (i != existing.end()) { - if (i->second != value) { + if (i->second.text != value.text) { return true; } existing.erase(i); - } else if (!value.isEmpty()) { + } else if (!value.text.isEmpty()) { return true; } } @@ -1018,7 +1056,6 @@ void FormController::saveValueEdit( base::take(nonconst->data.encryptedSecretInEdit); base::take(nonconst->data.hashInEdit); base::take(nonconst->data.parsedInEdit); - base::take(nonconst->error); nonconst->saveRequestId = 0; _valueSaveFinished.fire_copy(nonconst); }); @@ -1049,7 +1086,7 @@ void FormController::deleteValueEdit(not_null value) { _valueSaveFinished.fire_copy(value); }).fail([=](const RPCError &error) { nonconst->saveRequestId = 0; - valueSaveFailed(nonconst, error); + valueSaveShowError(nonconst, error); }).send(); } @@ -1090,7 +1127,7 @@ void FormController::saveEncryptedValue(not_null value) { value->data.secret = GenerateSecretBytes(); } const auto encryptedData = EncryptData( - SerializeData(value->data.parsedInEdit.fields), + SerializeData(GetTexts(value->data.parsedInEdit)), value->data.secret); value->data.hashInEdit = encryptedData.hash; value->data.encryptedSecretInEdit = EncryptValueSecret( @@ -1197,18 +1234,37 @@ void FormController::sendSaveRequest( _valueSaveFinished.fire_copy(value); }).fail([=](const RPCError &error) { value->saveRequestId = 0; - if (error.type() == qstr("PHONE_VERIFICATION_NEEDED")) { + const auto code = error.type(); + if (code == qstr("PHONE_VERIFICATION_NEEDED")) { if (value->type == Value::Type::Phone) { startPhoneVerification(value); return; } - } else if (error.type() == qstr("EMAIL_VERIFICATION_NEEDED")) { + } else if (code == qstr("PHONE_NUMBER_INVALID")) { + if (value->type == Value::Type::Phone) { + value->data.parsedInEdit.fields["value"].error + = lang(lng_bad_phone); + valueSaveFailed(value); + return; + } + } else if (code == qstr("EMAIL_VERIFICATION_NEEDED")) { if (value->type == Value::Type::Email) { startEmailVerification(value); return; } + } else if (code == qstr("EMAIL_INVALID")) { + if (value->type == Value::Type::Email) { + value->data.parsedInEdit.fields["value"].error + = lang(lng_cloud_password_bad_email); + valueSaveFailed(value); + return; + } + } + if (SaveErrorRequiresRestart(code)) { + suggestRestart(); + } else { + valueSaveShowError(value, error); } - valueSaveFailed(value, error); }).send(); } @@ -1233,7 +1289,7 @@ QString FormController::getPlainTextFromValue( const auto i = value->data.parsedInEdit.fields.find("value"); Assert(i != end(value->data.parsedInEdit.fields)); - return i->second; + return i->second.text; } void FormController::startPhoneVerification(not_null value) { @@ -1291,7 +1347,7 @@ void FormController::startPhoneVerification(not_null value) { _verificationNeeded.fire_copy(value); }).fail([=](const RPCError &error) { value->verification.requestId = 0; - valueSaveFailed(value, error); + valueSaveShowError(value, error); }).send(); } @@ -1308,7 +1364,7 @@ void FormController::startEmailVerification(not_null value) { : -1; _verificationNeeded.fire_copy(value); }).fail([=](const RPCError &error) { - valueSaveFailed(value, error); + valueSaveShowError(value, error); }).send(); } @@ -1326,10 +1382,15 @@ void FormController::requestPhoneCall(not_null value) { }).send(); } -void FormController::valueSaveFailed( +void FormController::valueSaveShowError( not_null value, const RPCError &error) { - _view->show(Box("Error saving value:\n" + error.type())); + _view->show(Box( + Lang::Hard::SecureSaveError() + "\n" + error.type())); + valueSaveFailed(value); +} + +void FormController::valueSaveFailed(not_null value) { valueEditFailed(value); _valueSaveFinished.fire_copy(value); } @@ -1378,16 +1439,24 @@ void FormController::generateSecret(bytes::const_span password) { callback(); } }).fail([=](const RPCError &error) { - // #TODO wrong password hash error? - Ui::show(Box("Saving encrypted value failed.")); _saveSecretRequestId = 0; + suggestRestart(); }).send(); } +void FormController::suggestRestart() { + _suggestingRestart = true; + _view->show(Box( + lang(lng_passport_restart_sure), + lang(lng_passport_restart), + [=] { _controller->showPassportForm(_request); }, + [=] { cancel(); })); +} + void FormController::requestForm() { if (_request.payload.isEmpty()) { _formRequestId = -1; - Ui::show(Box(lang(lng_passport_form_error))); + formFail("PAYLOAD_EMPTY"); return; } _formRequestId = request(MTPaccount_GetAuthorizationForm( @@ -1398,7 +1467,7 @@ void FormController::requestForm() { _formRequestId = 0; formDone(result); }).fail([=](const RPCError &error) { - formFail(error); + formFail(error.type()); }).send(); } @@ -1496,11 +1565,11 @@ auto FormController::parseValue( switch (data.vplain_data.type()) { case mtpc_securePlainPhone: { const auto &fields = data.vplain_data.c_securePlainPhone(); - result.data.parsed.fields["value"] = qs(fields.vphone); + result.data.parsed.fields["value"].text = qs(fields.vphone); } break; case mtpc_securePlainEmail: { const auto &fields = data.vplain_data.c_securePlainEmail(); - result.data.parsed.fields["value"] = qs(fields.vemail); + result.data.parsed.fields["value"].text = qs(fields.vemail); } break; } } @@ -1596,8 +1665,10 @@ void FormController::parseForm(const MTPaccount_AuthorizationForm &result) { _bot = App::userLoaded(_request.botId); } -void FormController::formFail(const RPCError &error) { - Ui::show(Box(lang(lng_passport_form_error))); +void FormController::formFail(const QString &error) { + _serviceErrorText = error; + _view->showCriticalError( + lang(lng_passport_form_error) + "\n" + error); } void FormController::requestPassword() { @@ -1606,7 +1677,7 @@ void FormController::requestPassword() { _passwordRequestId = 0; passwordDone(result); }).fail([=](const RPCError &error) { - formFail(error); + formFail(error.type()); }).send(); } @@ -1627,7 +1698,7 @@ void FormController::passwordDone(const MTPaccount_Password &result) { void FormController::showForm() { if (!_bot) { - Ui::show(Box("Could not get authorization bot.")); + formFail(Lang::Hard::NoAuthorizationBot()); return; } if (!_password.salt.empty()) { @@ -1639,10 +1710,6 @@ void FormController::showForm() { } } -void FormController::passwordFail(const RPCError &error) { - Ui::show(Box("Could not get authorization form.")); -} - void FormController::parsePassword(const MTPDaccount_noPassword &result) { _password.unconfirmedPattern = qs(result.vemail_unconfirmed_pattern); _password.newSalt = bytes::make_vector(result.vnew_salt.v); @@ -1661,25 +1728,41 @@ void FormController::parsePassword(const MTPDaccount_password &result) { } void FormController::cancel() { - if (!_submitSuccess) { + if (!_submitSuccess && _serviceErrorText.isEmpty()) { _view->show(Box( lang(lng_passport_stop_sure), lang(lng_passport_stop), - [=] { cancelSure(); })); + [=] { cancelSure(); }, + [=] { cancelAbort(); })); } else { cancelSure(); } } +void FormController::cancelAbort() { + if (_cancelled || _submitSuccess) { + return; + } else if (_suggestingRestart) { + suggestRestart(); + } +} + void FormController::cancelSure() { if (!_cancelled) { _cancelled = true; - const auto url = qthelp::url_append_query( - _request.callbackUrl, - _submitSuccess ? "tg_passport=success" : "tg_passport=cancel"); - UrlClickHandler::doOpen(url); - + if (!_request.callbackUrl.isEmpty() + && (_serviceErrorText.isEmpty() + || ForwardServiceErrorRequired(_serviceErrorText))) { + const auto url = qthelp::url_append_query( + _request.callbackUrl, + (_submitSuccess + ? "tg_passport=success" + : (_serviceErrorText.isEmpty() + ? "tg_passport=cancel" + : "tg_passport=error&error=" + _serviceErrorText))); + UrlClickHandler::doOpen(url); + } const auto timeout = _view->closeGetDuration(); App::CallDelayed(timeout, this, [=] { _controller->clearPassportForm(); diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index d3cb49812..903e3bd39 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -84,6 +84,7 @@ struct File { int downloadOffset = 0; QImage image; + QString error; }; struct EditFile { @@ -99,8 +100,13 @@ struct EditFile { bool deleted = false; }; +struct ValueField { + QString text; + QString error; +}; + struct ValueMap { - std::map fields; + std::map fields; }; struct ValueData { @@ -146,13 +152,13 @@ struct Value { ValueData data; std::vector scans; std::vector scansInEdit; + QString scanMissingError; base::optional selfie; base::optional selfieInEdit; Verification verification; bytes::vector submitHash; int editScreens = 0; - base::optional error; mtpRequestId saveRequestId = 0; }; @@ -208,7 +214,7 @@ public: void show(); UserData *bot() const; QString privacyPolicyUrl() const; - bool submit(); + std::vector> submitGetErrors(); void submitPassword(const QString &password); rpl::producer passwordError() const; QString passwordHint() const; @@ -253,6 +259,7 @@ private: struct FinalData { QVector hashes; QByteArray credentials; + std::vector> errors; }; EditFile *findEditFile(const FullMsgId &fullId); EditFile *findEditFile(const FileKey &key); @@ -263,7 +270,7 @@ private: void requestPassword(); void formDone(const MTPaccount_AuthorizationForm &result); - void formFail(const RPCError &error); + void formFail(const QString &error); void parseForm(const MTPaccount_AuthorizationForm &result); void showForm(); Value parseValue( @@ -280,7 +287,6 @@ private: const std::vector &source) const; void passwordDone(const MTPaccount_Password &result); - void passwordFail(const RPCError &error); void parsePassword(const MTPDaccount_noPassword &settings); void parsePassword(const MTPDaccount_password &settings); bytes::vector passwordHashForAuth(bytes::const_span password) const; @@ -327,7 +333,8 @@ private: 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 valueSaveShowError(not_null value, const RPCError &error); + void valueSaveFailed(not_null value); void requestPhoneCall(not_null value); void verificationError( not_null value, @@ -345,7 +352,9 @@ private: const MTPInputSecureValue &data); FinalData prepareFinalData(); + void suggestRestart(); void cancelSure(); + void cancelAbort(); not_null _controller; FormRequest _request; @@ -373,6 +382,8 @@ private: rpl::event_stream _passwordError; mtpRequestId _submitRequestId = 0; bool _submitSuccess = false; + bool _suggestingRestart = false; + QString _serviceErrorText; rpl::lifetime _uploaderSubscriptions; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp index 95df8af0e..83187c167 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp @@ -147,20 +147,26 @@ QString ComputeScopeRowReadyString(const Scope &scope) { const auto i = fields.find(row.key); if (i == end(fields)) { return QString(); - } else if (row.validate && !row.validate(i->second)) { + } + const auto text = i->second.text; + if (row.validate && !row.validate(text)) { return QString(); } - pushListValue(format ? format(i->second) : i->second); + pushListValue(format ? format(text) : text); + } else if (scope.documents.empty()) { + continue; } else if (!document) { return QString(); } else { const auto i = document->data.parsed.fields.find(row.key); if (i == end(document->data.parsed.fields)) { return QString(); - } else if (row.validate && !row.validate(i->second)) { + } + const auto text = i->second.text; + if (row.validate && !row.validate(text)) { return QString(); } - pushListValue(i->second); + pushListValue(text); } } return list.join(", "); @@ -171,7 +177,7 @@ QString ComputeScopeRowReadyString(const Scope &scope) { const auto &fields = scope.fields->data.parsed.fields; const auto i = fields.find("value"); return (i != end(fields)) - ? (format ? format(i->second) : i->second) + ? (format ? format(i->second.text) : i->second.text) : QString(); } break; } @@ -182,13 +188,14 @@ ScopeRow ComputeScopeRow(const Scope &scope) { const auto addReadyError = [&](ScopeRow &&row) { const auto ready = ComputeScopeRowReadyString(scope); row.ready = ready; - row.error = scope.fields->error.has_value() - ? (!scope.fields->error->isEmpty() - ? *scope.fields->error - : !ready.isEmpty() - ? ready - : row.description) - : QString(); + // #TODO passport bot errors + //row.error = scope.fields->error.has_value() + // ? (!scope.fields->error->isEmpty() + // ? *scope.fields->error + // : !ready.isEmpty() + // ? ready + // : row.description) + // : QString(); return row; }; switch (scope.type) { diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h index e951c9071..c9e916fe3 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h @@ -43,6 +43,7 @@ public: virtual void showAskPassword() = 0; virtual void showNoPassword() = 0; virtual void showPasswordUnconfirmed() = 0; + virtual void showCriticalError(const QString &error) = 0; virtual void editScope(int index) = 0; virtual void showBox(object_ptr box) = 0; diff --git a/Telegram/SourceFiles/passport/passport_panel.cpp b/Telegram/SourceFiles/passport/passport_panel.cpp index 5c7599652..7a47f254b 100644 --- a/Telegram/SourceFiles/passport/passport_panel.cpp +++ b/Telegram/SourceFiles/passport/passport_panel.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/wrap/padding_wrap.h" #include "ui/wrap/fade_wrap.h" #include "lang/lang_keys.h" #include "window/layer_widget.h" @@ -231,6 +232,23 @@ void Panel::showPasswordUnconfirmed() { setBackAllowed(false); } +void Panel::showCriticalError(const QString &error) { + auto container = base::make_unique_q>( + _body, + object_ptr( + _body, + error, + Ui::FlatLabel::InitType::Simple, + st::passportErrorLabel), + style::margins(0, st::passportPanelHeight / 3, 0, 0)); + container->widthValue( + ) | rpl::start_with_next([label = container->entity()](int width) { + label->resize(width, label->height()); + }, container->lifetime()); + showInner(std::move(container)); + setBackAllowed(false); +} + void Panel::showForm() { showInner(base::make_unique_q(_body, _controller)); setBackAllowed(false); @@ -484,7 +502,7 @@ void Panel::paintOpaqueBorder(Painter &p) const { } void Panel::closeEvent(QCloseEvent *e) { - // #TODO + // #TODO passport } void Panel::mousePressEvent(QMouseEvent *e) { diff --git a/Telegram/SourceFiles/passport/passport_panel.h b/Telegram/SourceFiles/passport/passport_panel.h index 953b0ece5..abb705f82 100644 --- a/Telegram/SourceFiles/passport/passport_panel.h +++ b/Telegram/SourceFiles/passport/passport_panel.h @@ -39,6 +39,7 @@ public: void showNoPassword(); void showPasswordUnconfirmed(); void showForm(); + void showCriticalError(const QString &error); void showEditValue(object_ptr form); void showBox(object_ptr box); diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index 425c282a0..dc0ecedc3 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -359,6 +359,13 @@ void PanelController::fillRows( } for (const auto &scope : _scopes) { const auto row = ComputeScopeRow(scope); + const auto main = scope.fields; + if (!row.ready.isEmpty()) { + _submitErrors.erase( + ranges::remove(_submitErrors, main), + _submitErrors.end()); + } + const auto submitError = base::contains(_submitErrors, main); callback( row.title, (!row.error.isEmpty() @@ -367,7 +374,7 @@ void PanelController::fillRows( ? row.ready : row.description), !row.ready.isEmpty(), - !row.error.isEmpty()); + !row.error.isEmpty() || submitError); } } @@ -380,7 +387,8 @@ rpl::producer<> PanelController::refillRows() const { } void PanelController::submitForm() { - if (!_form->submit()) { + _submitErrors = _form->submitGetErrors(); + if (!_submitErrors.empty()) { _submitFailed.fire({}); } } @@ -466,6 +474,10 @@ rpl::producer PanelController::scanUpdated() const { }); } +rpl::producer PanelController::saveErrors() const { + return _saveErrors.events(); +} + ScanInfo PanelController::collectScanInfo(const EditFile &file) const { Expects(_editScope != nullptr); Expects(_editDocument != nullptr); @@ -507,7 +519,34 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const { status, file.fields.image, file.deleted, - isSelfie }; + isSelfie, + file.fields.error }; +} + +std::vector PanelController::collectErrors( + not_null value) const { + auto result = std::vector(); + if (!value->scanMissingError.isEmpty()) { + result.push_back({ FileKey(), value->scanMissingError }); + } + const auto addFileError = [&](const EditFile &file) { + if (!file.fields.error.isEmpty()) { + const auto key = FileKey{ file.fields.id, file.fields.dcId }; + result.push_back({ key, file.fields.error }); + } + }; + for (const auto &scan : value->scansInEdit) { + addFileError(scan); + } + if (value->selfieInEdit) { + addFileError(*value->selfieInEdit); + } + for (const auto &[key, value] : value->data.parsedInEdit.fields) { + if (!value.error.isEmpty()) { + result.push_back({ key, value.error }); + } + } + return result; } auto PanelController::deleteValueLabel() const @@ -625,6 +664,11 @@ void PanelController::showPasswordUnconfirmed() { _panel->showPasswordUnconfirmed(); } +void PanelController::showCriticalError(const QString &error) { + ensurePanelCreated(); + _panel->showCriticalError(error); +} + void PanelController::ensurePanelCreated() { if (!_panel) { _panel = std::make_unique(this); @@ -783,7 +827,7 @@ void PanelController::editScope(int index, int documentIndex) { const auto valueIt = parsed.fields.find("value"); const auto value = (valueIt == end(parsed.fields) ? QString() - : valueIt->second); + : valueIt->second.text); const auto existing = getDefaultContactValue(_editScope->type); _panelHasUnsavedChanges = nullptr; return object_ptr( @@ -829,7 +873,13 @@ void PanelController::processValueSaveFinished( } if ((_editValue == value || _editDocument == value) && !savingScope()) { - _panel->showForm(); + if (auto errors = collectErrors(value); !errors.empty()) { + for (auto &&error : errors) { + _saveErrors.fire(std::move(error)); + } + } else { + _panel->showForm(); + } } } @@ -849,7 +899,7 @@ void PanelController::processVerificationNeeded( } const auto textIt = value->data.parsedInEdit.fields.find("value"); Assert(textIt != end(value->data.parsedInEdit.fields)); - const auto text = textIt->second; + const auto text = textIt->second.text; const auto type = value->type; const auto update = _form->verificationUpdate( ) | rpl::filter([=](not_null field) { diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h index 42eeeab97..5789d5771 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.h +++ b/Telegram/SourceFiles/passport/passport_panel_controller.h @@ -29,6 +29,16 @@ struct ScanInfo { QImage thumb; bool deleted = false; bool selfie = false; + QString error; + +}; + +struct ScopeError { + // FileKey:id != 0 - file_hash error (bad scan / selfie) + // FileKey:id == 0 - vector error (scan missing) + // QString - data_hash with such key error (bad value) + base::variant key; + QString text; }; @@ -68,6 +78,7 @@ public: void deleteSelfie(); void restoreSelfie(); rpl::producer scanUpdated() const; + rpl::producer saveErrors() const; base::optional> deleteValueLabel() const; void deleteValue(); @@ -78,6 +89,7 @@ public: void showAskPassword() override; void showNoPassword() override; void showPasswordUnconfirmed() override; + void showCriticalError(const QString &error) override; void fillRows( base::lambda collectErrors( + not_null value) const; QString getDefaultContactValue(Scope::Type type) const; void deleteValueSure(bool withDetails); not_null _form; std::vector _scopes; rpl::event_stream<> _submitFailed; + std::vector> _submitErrors; + rpl::event_stream _saveErrors; std::unique_ptr _panel; base::lambda _panelHasUnsavedChanges; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp index c0006b8c2..201fc7539 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp @@ -16,6 +16,7 @@ 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/slide_wrap.h" #include "ui/wrap/fade_wrap.h" #include "boxes/abstract_box.h" #include "boxes/confirm_phone_box.h" @@ -260,6 +261,18 @@ void PanelEditContact::setupControls( }, _field->lifetime()); _content->add(std::move(wrap), fieldPadding); + const auto errorWrap = _content->add( + object_ptr>( + _content, + object_ptr( + _content, + QString(), + Ui::FlatLabel::InitType::Simple, + st::passportVerifyErrorLabel), + st::passportContactErrorPadding), + st::passportContactErrorMargin); + errorWrap->hide(anim::type::instant); + _content->add( object_ptr( _content, @@ -282,12 +295,24 @@ void PanelEditContact::setupControls( }); } + _controller->saveErrors( + ) | rpl::start_with_next([=](const ScopeError &error) { + if (error.key == QString("value")) { + _field->showError(); + errorWrap->entity()->setText(error.text); + errorWrap->show(anim::type::normal); + } + }, lifetime()); + const auto submit = [=] { crl::on_main(this, [=] { save(); }); }; connect(_field, &Ui::MaskedInputField::submitted, submit); + connect(_field, &Ui::MaskedInputField::changed, [=] { + errorWrap->hide(anim::type::normal); + }); _done->addClickHandler(submit); } @@ -323,7 +348,7 @@ void PanelEditContact::save() { void PanelEditContact::save(const QString &value) { auto data = ValueMap(); - data.fields["value"] = value; + data.fields["value"].text = value; _controller->saveScope(std::move(data), {}); } diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index 6df493492..76adb22c2 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -305,7 +305,7 @@ not_null PanelEditDocument::setupContent( if (const auto i = fields.find(key); i != fields.end()) { return i->second; } - return QString(); + return ValueField(); }; for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) { @@ -316,13 +316,14 @@ not_null PanelEditDocument::setupContent( if (!fields) { continue; } + const auto current = valueOrEmpty(*fields, row.key); _details.emplace(i, inner->add(PanelDetailsRow::Create( inner, row.inputType, _controller, row.label, - valueOrEmpty(*fields, row.key), - QString(), + current.text, + current.error, row.lengthLimit))); } @@ -382,7 +383,7 @@ PanelEditDocument::Result PanelEditDocument::collect() const { auto &fields = (row.valueClass == Scheme::ValueClass::Fields) ? result.data : result.filesData; - fields.fields[row.key] = field->valueCurrent(); + fields.fields[row.key].text = field->valueCurrent(); } return result; }