Display errors on partial form submit.

This commit is contained in:
John Preston 2018-04-13 21:42:28 +04:00
parent 49578836be
commit 1064208be9
7 changed files with 159 additions and 90 deletions

View File

@ -209,7 +209,7 @@ bytes::vector FormController::passwordHashForAuth(
_password.salt)); _password.salt));
} }
auto FormController::prepareFinalData() const -> FinalData { auto FormController::prepareFinalData() -> FinalData {
auto hashes = QVector<MTPSecureValueHash>(); auto hashes = QVector<MTPSecureValueHash>();
auto secureData = QJsonObject(); auto secureData = QJsonObject();
const auto addValueToJSON = [&]( const auto addValueToJSON = [&](
@ -249,7 +249,7 @@ auto FormController::prepareFinalData() const -> FinalData {
const auto ready = ComputeScopeRowReadyString(scope); const auto ready = ComputeScopeRowReadyString(scope);
if (ready.isEmpty()) { if (ready.isEmpty()) {
hasErrors = true; hasErrors = true;
_valueError.fire_copy(scope.fields); findValue(scope.fields)->error = QString();
continue; continue;
} }
addValue(scope.fields); addValue(scope.fields);
@ -276,14 +276,14 @@ auto FormController::prepareFinalData() const -> FinalData {
}; };
} }
void FormController::submit() { bool FormController::submit() {
if (_submitRequestId) { if (_submitRequestId) {
return; return true;
} }
const auto prepared = prepareFinalData(); const auto prepared = prepareFinalData();
if (prepared.hashes.empty()) { if (prepared.hashes.empty()) {
return; return false;
} }
const auto credentialsEncryptedData = EncryptData( const auto credentialsEncryptedData = EncryptData(
bytes::make_span(prepared.credentials)); bytes::make_span(prepared.credentials));
@ -309,6 +309,7 @@ void FormController::submit() {
_view->show(Box<InformBox>( _view->show(Box<InformBox>(
"Failed sending data :(\n" + error.type())); "Failed sending data :(\n" + error.type()));
}).send(); }).send();
return true;
} }
void FormController::submitPassword(const QString &password) { void FormController::submitPassword(const QString &password) {
@ -691,11 +692,6 @@ auto FormController::valueSaveFinished() const
return _valueSaveFinished.events(); return _valueSaveFinished.events();
} }
auto FormController::valueError() const
-> rpl::producer<not_null<const Value*>> {
return _valueError.events();
}
auto FormController::verificationNeeded() const auto FormController::verificationNeeded() const
-> rpl::producer<not_null<const Value*>> { -> rpl::producer<not_null<const Value*>> {
return _verificationNeeded.events(); return _verificationNeeded.events();
@ -990,7 +986,7 @@ bool FormController::editValueChanged(
void FormController::saveValueEdit( void FormController::saveValueEdit(
not_null<const Value*> value, not_null<const Value*> value,
ValueMap &&data) { ValueMap &&data) {
if (savingValue(value)) { if (savingValue(value) || _submitRequestId) {
return; return;
} }
@ -1003,6 +999,7 @@ void FormController::saveValueEdit(
base::take(nonconst->data.encryptedSecretInEdit); base::take(nonconst->data.encryptedSecretInEdit);
base::take(nonconst->data.hashInEdit); base::take(nonconst->data.hashInEdit);
base::take(nonconst->data.parsedInEdit); base::take(nonconst->data.parsedInEdit);
base::take(nonconst->error);
nonconst->saveRequestId = 0; nonconst->saveRequestId = 0;
_valueSaveFinished.fire_copy(nonconst); _valueSaveFinished.fire_copy(nonconst);
}); });
@ -1018,7 +1015,7 @@ void FormController::saveValueEdit(
} }
void FormController::deleteValueEdit(not_null<const Value*> value) { void FormController::deleteValueEdit(not_null<const Value*> value) {
if (savingValue(value)) { if (savingValue(value) || _submitRequestId) {
return; return;
} }

View File

@ -152,6 +152,7 @@ struct Value {
bytes::vector submitHash; bytes::vector submitHash;
int editScreens = 0; int editScreens = 0;
base::optional<QString> error;
mtpRequestId saveRequestId = 0; mtpRequestId saveRequestId = 0;
}; };
@ -207,7 +208,7 @@ public:
void show(); void show();
UserData *bot() const; UserData *bot() const;
QString privacyPolicyUrl() const; QString privacyPolicyUrl() const;
void submit(); bool submit();
void submitPassword(const QString &password); void submitPassword(const QString &password);
rpl::producer<QString> passwordError() const; rpl::producer<QString> passwordError() const;
QString passwordHint() const; QString passwordHint() const;
@ -227,7 +228,6 @@ public:
rpl::producer<not_null<const EditFile*>> scanUpdated() const; rpl::producer<not_null<const EditFile*>> scanUpdated() const;
rpl::producer<not_null<const Value*>> valueSaveFinished() 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*>> verificationNeeded() const;
rpl::producer<not_null<const Value*>> verificationUpdate() const; rpl::producer<not_null<const Value*>> verificationUpdate() const;
void verify(not_null<const Value*> value, const QString &code); void verify(not_null<const Value*> value, const QString &code);
@ -342,7 +342,7 @@ private:
void sendSaveRequest( void sendSaveRequest(
not_null<Value*> value, not_null<Value*> value,
const MTPInputSecureValue &data); const MTPInputSecureValue &data);
FinalData prepareFinalData() const; FinalData prepareFinalData();
not_null<Window::Controller*> _controller; not_null<Window::Controller*> _controller;
FormRequest _request; FormRequest _request;
@ -359,7 +359,6 @@ private:
rpl::event_stream<not_null<const EditFile*>> _scanUpdated; rpl::event_stream<not_null<const EditFile*>> _scanUpdated;
rpl::event_stream<not_null<const Value*>> _valueSaveFinished; 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*>> _verificationNeeded;
rpl::event_stream<not_null<const Value*>> _verificationUpdate; rpl::event_stream<not_null<const Value*>> _verificationUpdate;

View File

@ -174,89 +174,89 @@ QString ComputeScopeRowReadyString(const Scope &scope) {
} }
ScopeRow ComputeScopeRow(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) { switch (scope.type) {
case Scope::Type::Identity: case Scope::Type::Identity:
if (scope.documents.empty()) { if (scope.documents.empty()) {
return { return addReadyError({
lang(lng_passport_personal_details), lang(lng_passport_personal_details),
lang(lng_passport_personal_details_enter), lang(lng_passport_personal_details_enter),
ComputeScopeRowReadyString(scope) });
};
} else if (scope.documents.size() == 1) { } else if (scope.documents.size() == 1) {
switch (scope.documents.front()->type) { switch (scope.documents.front()->type) {
case Value::Type::Passport: case Value::Type::Passport:
return { return addReadyError({
lang(lng_passport_identity_passport), lang(lng_passport_identity_passport),
lang(lng_passport_identity_passport_upload), lang(lng_passport_identity_passport_upload),
ComputeScopeRowReadyString(scope) });
};
case Value::Type::IdentityCard: case Value::Type::IdentityCard:
return { return addReadyError({
lang(lng_passport_identity_card), lang(lng_passport_identity_card),
lang(lng_passport_identity_card_upload), lang(lng_passport_identity_card_upload),
ComputeScopeRowReadyString(scope) });
};
case Value::Type::DriverLicense: case Value::Type::DriverLicense:
return { return addReadyError({
lang(lng_passport_identity_license), lang(lng_passport_identity_license),
lang(lng_passport_identity_license_upload), lang(lng_passport_identity_license_upload),
ComputeScopeRowReadyString(scope) });
};
default: Unexpected("Identity type in ComputeScopeRow."); default: Unexpected("Identity type in ComputeScopeRow.");
} }
} }
return { return addReadyError({
lang(lng_passport_identity_title), lang(lng_passport_identity_title),
lang(lng_passport_identity_description), lang(lng_passport_identity_description),
ComputeScopeRowReadyString(scope) });
};
case Scope::Type::Address: case Scope::Type::Address:
if (scope.documents.empty()) { if (scope.documents.empty()) {
return { return addReadyError({
lang(lng_passport_address), lang(lng_passport_address),
lang(lng_passport_address_enter), lang(lng_passport_address_enter),
ComputeScopeRowReadyString(scope) });
};
} else if (scope.documents.size() == 1) { } else if (scope.documents.size() == 1) {
switch (scope.documents.front()->type) { switch (scope.documents.front()->type) {
case Value::Type::BankStatement: case Value::Type::BankStatement:
return { return addReadyError({
lang(lng_passport_address_statement), lang(lng_passport_address_statement),
lang(lng_passport_address_statement_upload), lang(lng_passport_address_statement_upload),
ComputeScopeRowReadyString(scope) });
};
case Value::Type::UtilityBill: case Value::Type::UtilityBill:
return { return addReadyError({
lang(lng_passport_address_bill), lang(lng_passport_address_bill),
lang(lng_passport_address_bill_upload), lang(lng_passport_address_bill_upload),
ComputeScopeRowReadyString(scope) });
};
case Value::Type::RentalAgreement: case Value::Type::RentalAgreement:
return { return addReadyError({
lang(lng_passport_address_agreement), lang(lng_passport_address_agreement),
lang(lng_passport_address_agreement_upload), lang(lng_passport_address_agreement_upload),
ComputeScopeRowReadyString(scope) });
};
default: Unexpected("Address type in ComputeScopeRow."); default: Unexpected("Address type in ComputeScopeRow.");
} }
} }
return { return addReadyError({
lang(lng_passport_address_title), lang(lng_passport_address_title),
lang(lng_passport_address_description), lang(lng_passport_address_description),
ComputeScopeRowReadyString(scope) });
};
case Scope::Type::Phone: case Scope::Type::Phone:
return { return addReadyError({
lang(lng_passport_phone_title), lang(lng_passport_phone_title),
lang(lng_passport_phone_description), lang(lng_passport_phone_description),
ComputeScopeRowReadyString(scope) });
};
case Scope::Type::Email: case Scope::Type::Email:
return { return addReadyError({
lang(lng_passport_email_title), lang(lng_passport_email_title),
lang(lng_passport_email_description), lang(lng_passport_email_description),
ComputeScopeRowReadyString(scope) });
};
default: Unexpected("Scope type in ComputeScopeRow."); default: Unexpected("Scope type in ComputeScopeRow.");
} }
} }

View File

@ -30,6 +30,7 @@ struct ScopeRow {
QString title; QString title;
QString description; QString description;
QString ready; QString ready;
QString error;
}; };
std::vector<Scope> ComputeScopes( std::vector<Scope> ComputeScopes(

View File

@ -352,7 +352,8 @@ void PanelController::fillRows(
base::lambda<void( base::lambda<void(
QString title, QString title,
QString description, QString description,
bool ready)> callback) { bool ready,
bool error)> callback) {
if (_scopes.empty()) { if (_scopes.empty()) {
_scopes = ComputeScopes(_form); _scopes = ComputeScopes(_form);
} }
@ -360,13 +361,28 @@ void PanelController::fillRows(
const auto row = ComputeScopeRow(scope); const auto row = ComputeScopeRow(scope);
callback( callback(
row.title, row.title,
row.ready.isEmpty() ? row.description : row.ready, (!row.error.isEmpty()
!row.ready.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() { void PanelController::submitForm() {
_form->submit(); if (!_form->submit()) {
_submitFailed.fire({});
}
} }
void PanelController::submitPassword(const QString &password) { void PanelController::submitPassword(const QString &password) {
@ -812,7 +828,7 @@ void PanelController::processValueSaveFinished(
_verificationBoxes.erase(boxIt); _verificationBoxes.erase(boxIt);
} }
if (!savingScope()) { if ((_editValue == value || _editDocument == value) && !savingScope()) {
_panel->showForm(); _panel->showForm();
} }
} }

View File

@ -83,7 +83,9 @@ public:
base::lambda<void( base::lambda<void(
QString title, QString title,
QString description, QString description,
bool ready)> callback); bool ready,
bool error)> callback);
rpl::producer<> refillRows() const;
void editScope(int index) override; void editScope(int index) override;
void saveScope(ValueMap &&data, ValueMap &&filesData); void saveScope(ValueMap &&data, ValueMap &&filesData);
@ -124,6 +126,7 @@ private:
not_null<FormController*> _form; not_null<FormController*> _form;
std::vector<Scope> _scopes; std::vector<Scope> _scopes;
rpl::event_stream<> _submitFailed;
std::unique_ptr<Panel> _panel; std::unique_ptr<Panel> _panel;
base::lambda<bool()> _panelHasUnsavedChanges; base::lambda<bool()> _panelHasUnsavedChanges;

View File

@ -27,12 +27,14 @@ namespace Passport {
class PanelForm::Row : public Ui::RippleButton { class PanelForm::Row : public Ui::RippleButton {
public: public:
Row( explicit Row(QWidget *parent);
QWidget *parent,
const QString &title,
const QString &description);
void setReady(bool ready); void updateContent(
const QString &title,
const QString &description,
bool ready,
bool error,
anim::type animated);
protected: protected:
int resizeGetHeight(int newWidth) override; int resizeGetHeight(int newWidth) override;
@ -48,28 +50,44 @@ private:
int _titleHeight = 0; int _titleHeight = 0;
int _descriptionHeight = 0; int _descriptionHeight = 0;
bool _ready = false; bool _ready = false;
bool _error = false;
Animation _errorAnimation;
}; };
PanelForm::Row::Row( PanelForm::Row::Row(QWidget *parent)
QWidget *parent,
const QString &title,
const QString &description)
: RippleButton(parent, st::passportRowRipple) : RippleButton(parent, st::passportRowRipple)
, _title( , _title(st::boxWideWidth / 2)
st::semiboldTextStyle, , _description(st::boxWideWidth / 2) {
title,
Ui::NameTextOptions(),
st::boxWideWidth / 2)
, _description(
st::defaultTextStyle,
description,
Ui::NameTextOptions(),
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; _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()); resizeToWidth(width());
update(); update();
} }
@ -110,22 +128,36 @@ void PanelForm::Row::paintEvent(QPaintEvent *e) {
const auto availableWidth = countAvailableWidth(); const auto availableWidth = countAvailableWidth();
auto top = st::passportRowPadding.top(); auto top = st::passportRowPadding.top();
const auto error = _errorAnimation.current(ms, _error ? 1. : 0.);
p.setPen(st::passportRowTitleFg); p.setPen(st::passportRowTitleFg);
_title.drawLeft(p, left, top, availableWidth, width()); _title.drawLeft(p, left, top, availableWidth, width());
top += _titleHeight + st::passportRowSkip; top += _titleHeight + st::passportRowSkip;
p.setPen(st::passportRowDescriptionFg); p.setPen(anim::pen(
st::passportRowDescriptionFg,
st::boxTextFgError,
error));
_description.drawLeft(p, left, top, availableWidth, width()); _description.drawLeft(p, left, top, availableWidth, width());
top += _descriptionHeight + st::passportRowPadding.bottom(); top += _descriptionHeight + st::passportRowPadding.bottom();
const auto &icon = _ready const auto &icon = _ready
? st::passportRowReadyIcon ? st::passportRowReadyIcon
: st::passportRowEmptyIcon; : st::passportRowEmptyIcon;
icon.paint( if (error > 0. && !_ready) {
p, icon.paint(
width() - st::passportRowPadding.right() - icon.width(), p,
(height() - icon.height()) / 2, width() - st::passportRowPadding.right() - icon.width(),
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( PanelForm::PanelForm(
@ -223,17 +255,38 @@ not_null<Ui::RpWidget*> PanelForm::setupContent() {
_controller->fillRows([&]( _controller->fillRows([&](
QString title, QString title,
QString description, QString description,
bool ready) { bool ready,
_rows.push_back(inner->add(object_ptr<Row>( bool error) {
this, _rows.push_back(inner->add(object_ptr<Row>(this)));
title,
description)));
_rows.back()->addClickHandler([=] { _rows.back()->addClickHandler([=] {
_controller->editScope(index); _controller->editScope(index);
}); });
_rows.back()->setReady(ready); _rows.back()->updateContent(
title,
description,
ready,
error,
anim::type::instant);
++index; ++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 policyUrl = _controller->privacyPolicyUrl();
const auto policy = inner->add( const auto policy = inner->add(
object_ptr<Ui::FlatLabel>( object_ptr<Ui::FlatLabel>(