From 1064208be9e66eeda7848abdb86647fb69df83db Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 13 Apr 2018 21:42:28 +0400 Subject: [PATCH] Display errors on partial form submit. --- .../passport/passport_form_controller.cpp | 21 ++-- .../passport/passport_form_controller.h | 7 +- .../passport_form_view_controller.cpp | 72 +++++------ .../passport/passport_form_view_controller.h | 1 + .../passport/passport_panel_controller.cpp | 26 +++- .../passport/passport_panel_controller.h | 5 +- .../passport/passport_panel_form.cpp | 117 +++++++++++++----- 7 files changed, 159 insertions(+), 90 deletions(-) diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index 540755d85..04fbddad4 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -209,7 +209,7 @@ bytes::vector FormController::passwordHashForAuth( _password.salt)); } -auto FormController::prepareFinalData() const -> FinalData { +auto FormController::prepareFinalData() -> FinalData { auto hashes = QVector<MTPSecureValueHash>(); auto secureData = QJsonObject(); const auto addValueToJSON = [&]( @@ -249,7 +249,7 @@ auto FormController::prepareFinalData() const -> FinalData { const auto ready = ComputeScopeRowReadyString(scope); if (ready.isEmpty()) { hasErrors = true; - _valueError.fire_copy(scope.fields); + findValue(scope.fields)->error = QString(); continue; } addValue(scope.fields); @@ -276,14 +276,14 @@ auto FormController::prepareFinalData() const -> FinalData { }; } -void FormController::submit() { +bool FormController::submit() { if (_submitRequestId) { - return; + return true; } const auto prepared = prepareFinalData(); if (prepared.hashes.empty()) { - return; + return false; } const auto credentialsEncryptedData = EncryptData( bytes::make_span(prepared.credentials)); @@ -309,6 +309,7 @@ void FormController::submit() { _view->show(Box<InformBox>( "Failed sending data :(\n" + error.type())); }).send(); + return true; } void FormController::submitPassword(const QString &password) { @@ -691,11 +692,6 @@ auto FormController::valueSaveFinished() const return _valueSaveFinished.events(); } -auto FormController::valueError() const --> rpl::producer<not_null<const Value*>> { - return _valueError.events(); -} - auto FormController::verificationNeeded() const -> rpl::producer<not_null<const Value*>> { return _verificationNeeded.events(); @@ -990,7 +986,7 @@ bool FormController::editValueChanged( void FormController::saveValueEdit( not_null<const Value*> value, ValueMap &&data) { - if (savingValue(value)) { + if (savingValue(value) || _submitRequestId) { return; } @@ -1003,6 +999,7 @@ 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); }); @@ -1018,7 +1015,7 @@ void FormController::saveValueEdit( } void FormController::deleteValueEdit(not_null<const Value*> value) { - if (savingValue(value)) { + if (savingValue(value) || _submitRequestId) { return; } diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index d33f0bc8c..cb53ba904 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -152,6 +152,7 @@ struct Value { bytes::vector submitHash; int editScreens = 0; + base::optional<QString> error; mtpRequestId saveRequestId = 0; }; @@ -207,7 +208,7 @@ public: void show(); UserData *bot() const; QString privacyPolicyUrl() const; - void submit(); + bool submit(); void submitPassword(const QString &password); rpl::producer<QString> passwordError() const; QString passwordHint() const; @@ -227,7 +228,6 @@ public: rpl::producer<not_null<const EditFile*>> scanUpdated() const; rpl::producer<not_null<const Value*>> valueSaveFinished() const; - rpl::producer<not_null<const Value*>> valueError() const; rpl::producer<not_null<const Value*>> verificationNeeded() const; rpl::producer<not_null<const Value*>> verificationUpdate() const; void verify(not_null<const Value*> value, const QString &code); @@ -342,7 +342,7 @@ private: void sendSaveRequest( not_null<Value*> value, const MTPInputSecureValue &data); - FinalData prepareFinalData() const; + FinalData prepareFinalData(); not_null<Window::Controller*> _controller; FormRequest _request; @@ -359,7 +359,6 @@ private: rpl::event_stream<not_null<const EditFile*>> _scanUpdated; rpl::event_stream<not_null<const Value*>> _valueSaveFinished; - rpl::event_stream<not_null<const Value*>> _valueError; rpl::event_stream<not_null<const Value*>> _verificationNeeded; rpl::event_stream<not_null<const Value*>> _verificationUpdate; diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp index 48f1a1258..4004ce92c 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp @@ -174,89 +174,89 @@ QString ComputeScopeRowReadyString(const Scope &scope) { } 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(); + return row; + }; switch (scope.type) { case Scope::Type::Identity: if (scope.documents.empty()) { - return { + return addReadyError({ lang(lng_passport_personal_details), lang(lng_passport_personal_details_enter), - ComputeScopeRowReadyString(scope) - }; + }); } else if (scope.documents.size() == 1) { switch (scope.documents.front()->type) { case Value::Type::Passport: - return { + return addReadyError({ lang(lng_passport_identity_passport), lang(lng_passport_identity_passport_upload), - ComputeScopeRowReadyString(scope) - }; + }); case Value::Type::IdentityCard: - return { + return addReadyError({ lang(lng_passport_identity_card), lang(lng_passport_identity_card_upload), - ComputeScopeRowReadyString(scope) - }; + }); case Value::Type::DriverLicense: - return { + return addReadyError({ lang(lng_passport_identity_license), lang(lng_passport_identity_license_upload), - ComputeScopeRowReadyString(scope) - }; + }); default: Unexpected("Identity type in ComputeScopeRow."); } } - return { + return addReadyError({ lang(lng_passport_identity_title), lang(lng_passport_identity_description), - ComputeScopeRowReadyString(scope) - }; + }); case Scope::Type::Address: if (scope.documents.empty()) { - return { + return addReadyError({ lang(lng_passport_address), lang(lng_passport_address_enter), - ComputeScopeRowReadyString(scope) - }; + }); } else if (scope.documents.size() == 1) { switch (scope.documents.front()->type) { case Value::Type::BankStatement: - return { + return addReadyError({ lang(lng_passport_address_statement), lang(lng_passport_address_statement_upload), - ComputeScopeRowReadyString(scope) - }; + }); case Value::Type::UtilityBill: - return { + return addReadyError({ lang(lng_passport_address_bill), lang(lng_passport_address_bill_upload), - ComputeScopeRowReadyString(scope) - }; + }); case Value::Type::RentalAgreement: - return { + return addReadyError({ lang(lng_passport_address_agreement), lang(lng_passport_address_agreement_upload), - ComputeScopeRowReadyString(scope) - }; + }); default: Unexpected("Address type in ComputeScopeRow."); } } - return { + return addReadyError({ lang(lng_passport_address_title), lang(lng_passport_address_description), - ComputeScopeRowReadyString(scope) - }; + }); case Scope::Type::Phone: - return { + return addReadyError({ lang(lng_passport_phone_title), lang(lng_passport_phone_description), - ComputeScopeRowReadyString(scope) - }; + }); case Scope::Type::Email: - return { + return addReadyError({ lang(lng_passport_email_title), lang(lng_passport_email_description), - ComputeScopeRowReadyString(scope) - }; + }); default: Unexpected("Scope type in ComputeScopeRow."); } } diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h index ac3e9398d..6edff2074 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h @@ -30,6 +30,7 @@ struct ScopeRow { QString title; QString description; QString ready; + QString error; }; std::vector<Scope> ComputeScopes( diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index 7cb1f8171..8a0033d14 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -352,7 +352,8 @@ void PanelController::fillRows( base::lambda<void( QString title, QString description, - bool ready)> callback) { + bool ready, + bool error)> callback) { if (_scopes.empty()) { _scopes = ComputeScopes(_form); } @@ -360,13 +361,28 @@ void PanelController::fillRows( const auto row = ComputeScopeRow(scope); callback( row.title, - row.ready.isEmpty() ? row.description : row.ready, - !row.ready.isEmpty()); + (!row.error.isEmpty() + ? row.error + : !row.ready.isEmpty() + ? row.ready + : row.description), + !row.ready.isEmpty(), + !row.error.isEmpty()); } } +rpl::producer<> PanelController::refillRows() const { + return rpl::merge( + _submitFailed.events(), + _form->valueSaveFinished() | rpl::map([] { + return rpl::empty_value(); + })); +} + void PanelController::submitForm() { - _form->submit(); + if (!_form->submit()) { + _submitFailed.fire({}); + } } void PanelController::submitPassword(const QString &password) { @@ -812,7 +828,7 @@ void PanelController::processValueSaveFinished( _verificationBoxes.erase(boxIt); } - if (!savingScope()) { + if ((_editValue == value || _editDocument == value) && !savingScope()) { _panel->showForm(); } } diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h index dec06d49e..b1b87664a 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.h +++ b/Telegram/SourceFiles/passport/passport_panel_controller.h @@ -83,7 +83,9 @@ public: base::lambda<void( QString title, QString description, - bool ready)> callback); + bool ready, + bool error)> callback); + rpl::producer<> refillRows() const; void editScope(int index) override; void saveScope(ValueMap &&data, ValueMap &&filesData); @@ -124,6 +126,7 @@ private: not_null<FormController*> _form; std::vector<Scope> _scopes; + rpl::event_stream<> _submitFailed; std::unique_ptr<Panel> _panel; base::lambda<bool()> _panelHasUnsavedChanges; diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index be5af5821..af3258b03 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -27,12 +27,14 @@ namespace Passport { class PanelForm::Row : public Ui::RippleButton { public: - Row( - QWidget *parent, - const QString &title, - const QString &description); + explicit Row(QWidget *parent); - void setReady(bool ready); + void updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated); protected: int resizeGetHeight(int newWidth) override; @@ -48,28 +50,44 @@ private: int _titleHeight = 0; int _descriptionHeight = 0; bool _ready = false; + bool _error = false; + Animation _errorAnimation; }; -PanelForm::Row::Row( - QWidget *parent, - const QString &title, - const QString &description) +PanelForm::Row::Row(QWidget *parent) : RippleButton(parent, st::passportRowRipple) -, _title( - st::semiboldTextStyle, - title, - Ui::NameTextOptions(), - st::boxWideWidth / 2) -, _description( - st::defaultTextStyle, - description, - Ui::NameTextOptions(), - st::boxWideWidth / 2) { +, _title(st::boxWideWidth / 2) +, _description(st::boxWideWidth / 2) { } -void PanelForm::Row::setReady(bool ready) { +void PanelForm::Row::updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated) { + _title.setText( + st::semiboldTextStyle, + title, + Ui::NameTextOptions()); + _description.setText( + st::defaultTextStyle, + description, + Ui::NameTextOptions()); _ready = ready; + if (_error != error) { + _error = error; + if (animated == anim::type::instant) { + _errorAnimation.finish(); + } else { + _errorAnimation.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + st::fadeWrapDuration); + } + } resizeToWidth(width()); update(); } @@ -110,22 +128,36 @@ void PanelForm::Row::paintEvent(QPaintEvent *e) { const auto availableWidth = countAvailableWidth(); auto top = st::passportRowPadding.top(); + const auto error = _errorAnimation.current(ms, _error ? 1. : 0.); + p.setPen(st::passportRowTitleFg); _title.drawLeft(p, left, top, availableWidth, width()); top += _titleHeight + st::passportRowSkip; - p.setPen(st::passportRowDescriptionFg); + p.setPen(anim::pen( + st::passportRowDescriptionFg, + st::boxTextFgError, + error)); _description.drawLeft(p, left, top, availableWidth, width()); top += _descriptionHeight + st::passportRowPadding.bottom(); const auto &icon = _ready ? st::passportRowReadyIcon : st::passportRowEmptyIcon; - icon.paint( - p, - width() - st::passportRowPadding.right() - icon.width(), - (height() - icon.height()) / 2, - width()); + if (error > 0. && !_ready) { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width(), + anim::color(st::menuIconFgOver, st::boxTextFgError, error)); + } else { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width()); + } } PanelForm::PanelForm( @@ -223,17 +255,38 @@ not_null<Ui::RpWidget*> PanelForm::setupContent() { _controller->fillRows([&]( QString title, QString description, - bool ready) { - _rows.push_back(inner->add(object_ptr<Row>( - this, - title, - description))); + bool ready, + bool error) { + _rows.push_back(inner->add(object_ptr<Row>(this))); _rows.back()->addClickHandler([=] { _controller->editScope(index); }); - _rows.back()->setReady(ready); + _rows.back()->updateContent( + title, + description, + ready, + error, + anim::type::instant); ++index; }); + _controller->refillRows( + ) | rpl::start_with_next([=] { + auto index = 0; + _controller->fillRows([&]( + QString title, + QString description, + bool ready, + bool error) { + Expects(index < _rows.size()); + + _rows[index++]->updateContent( + title, + description, + ready, + error, + anim::type::normal); + }); + }, lifetime()); const auto policyUrl = _controller->privacyPolicyUrl(); const auto policy = inner->add( object_ptr<Ui::FlatLabel>(