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