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));
}
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;
}

View File

@ -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;

View File

@ -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.");
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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>(