From f8b2e474b973311fec71f2cccf1f6923aded2dc4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 13 Apr 2018 14:10:09 +0400
Subject: [PATCH] Validate passport values before saving.

---
 .../passport/passport_panel_details_row.cpp   |  88 ++++++++++++--
 .../passport/passport_panel_details_row.h     |   1 +
 .../passport/passport_panel_edit_document.cpp |  22 ++++
 .../passport/passport_panel_edit_document.h   |   1 +
 Telegram/SourceFiles/ui/widgets/checkbox.cpp  | 111 ++++++++++++++++--
 Telegram/SourceFiles/ui/widgets/checkbox.h    |  87 ++++++++++++--
 6 files changed, 284 insertions(+), 26 deletions(-)

diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp b/Telegram/SourceFiles/passport/passport_panel_details_row.cpp
index 1cd21f44a..7cae5285b 100644
--- a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_details_row.cpp
@@ -166,14 +166,26 @@ private:
 	static QString GenderToString(Gender gender);
 
 	int resizeInner(int left, int top, int width) override;
+
 	void showInnerError() override;
 	void finishInnerAnimating() override;
+	void toggleError(bool shown);
+	void hideGenderError();
+	void errorAnimationCallback();
+
+	std::unique_ptr<Ui::AbstractCheckView> createRadioView(
+		Ui::RadioView* &weak) const;
 
 	std::shared_ptr<Ui::RadioenumGroup<Gender>> _group;
+	Ui::RadioView *_maleRadio = nullptr;
+	Ui::RadioView *_femaleRadio = nullptr;
 	object_ptr<Ui::Radioenum<Gender>> _male;
 	object_ptr<Ui::Radioenum<Gender>> _female;
 	rpl::variable<QString> _value;
 
+	bool _errorShown = false;
+	Animation _errorAnimation;
+
 };
 
 TextRow::TextRow(
@@ -280,7 +292,7 @@ void CountryRow::toggleError(bool shown) {
 void CountryRow::errorAnimationCallback() {
 	const auto error = _errorAnimation.current(_errorShown ? 1. : 0.);
 	if (error == 0.) {
-		_link->setColorOverride(nullptr);
+		_link->setColorOverride(base::none);
 	} else {
 		_link->setColorOverride(anim::color(
 			st::boxLinkButton.color,
@@ -365,7 +377,7 @@ rpl::producer<QChar> DateInput::putNext() const {
 void DateInput::keyPressEvent(QKeyEvent *e) {
 	const auto isBackspace = (e->key() == Qt::Key_Backspace);
 	const auto isBeginning = (cursorPosition() == 0);
-	if (isBackspace && isBeginning) {
+	if (isBackspace && isBeginning && !hasSelectedText()) {
 		_erasePrevious.fire({});
 	} else {
 		MaskedInputField::keyPressEvent(e);
@@ -470,12 +482,18 @@ DateRow::DateRow(
 	const auto blurred = [=] {
 		setFocused(false);
 	};
+	const auto changed = [=] {
+		_value = valueCurrent();
+	};
 	connect(_day, &Ui::MaskedInputField::focused, focused(_day));
 	connect(_month, &Ui::MaskedInputField::focused, focused(_month));
 	connect(_year, &Ui::MaskedInputField::focused, focused(_year));
 	connect(_day, &Ui::MaskedInputField::blurred, blurred);
 	connect(_month, &Ui::MaskedInputField::blurred, blurred);
 	connect(_year, &Ui::MaskedInputField::blurred, blurred);
+	connect(_day, &Ui::MaskedInputField::changed, changed);
+	connect(_month, &Ui::MaskedInputField::changed, changed);
+	connect(_year, &Ui::MaskedInputField::changed, changed);
 	_day->setMaxValue(31);
 	_day->putNext() | rpl::start_with_next([=](QChar ch) {
 		putNext(_month, ch);
@@ -494,6 +512,11 @@ DateRow::DateRow(
 	_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
 	_separator2->setAttribute(Qt::WA_TransparentForMouseEvents);
 	setMouseTracking(true);
+
+	_value.changes(
+	) | rpl::start_with_next([=] {
+		setErrorShown(false);
+	}, lifetime());
 }
 
 void DateRow::putNext(const object_ptr<DateInput> &field, QChar ch) {
@@ -748,19 +771,29 @@ GenderRow::GenderRow(
 	_group,
 	Gender::Male,
 	lang(lng_passport_gender_male),
-	st::defaultCheckbox)
+	st::defaultCheckbox,
+	createRadioView(_maleRadio))
 , _female(
 	this,
 	_group,
 	Gender::Female,
 	lang(lng_passport_gender_female),
-	st::defaultCheckbox)
+	st::defaultCheckbox,
+	createRadioView(_femaleRadio))
 , _value(StringToGender(value) ? value : QString()) {
 	_group->setChangedCallback([=](Gender gender) {
 		_value = GenderToString(gender);
+		hideGenderError();
 	});
 }
 
+std::unique_ptr<Ui::AbstractCheckView> GenderRow::createRadioView(
+		Ui::RadioView* &weak) const {
+	auto result = std::make_unique<Ui::RadioView>(st::defaultRadio, false);
+	weak = result.get();
+	return result;
+}
+
 auto GenderRow::StringToGender(const QString &value)
 -> base::optional<Gender> {
 	if (value == qstr("male")) {
@@ -793,9 +826,44 @@ int GenderRow::resizeInner(int left, int top, int width) {
 }
 
 void GenderRow::showInnerError() {
+	toggleError(true);
 }
 
 void GenderRow::finishInnerAnimating() {
+	if (_errorAnimation.animating()) {
+		_errorAnimation.finish();
+		errorAnimationCallback();
+	}
+}
+
+void GenderRow::hideGenderError() {
+	toggleError(false);
+}
+
+void GenderRow::toggleError(bool shown) {
+	if (_errorShown != shown) {
+		_errorShown = shown;
+		_errorAnimation.start(
+			[=] { errorAnimationCallback(); },
+			_errorShown ? 0. : 1.,
+			_errorShown ? 1. : 0.,
+			st::passportDetailsField.duration);
+	}
+}
+
+void GenderRow::errorAnimationCallback() {
+	const auto error = _errorAnimation.current(_errorShown ? 1. : 0.);
+	if (error == 0.) {
+		_maleRadio->setUntoggledOverride(base::none);
+		_femaleRadio->setUntoggledOverride(base::none);
+	} else {
+		const auto color = anim::color(
+			st::defaultRadio.untoggledFg,
+			st::boxTextFgError,
+			error);
+		_maleRadio->setUntoggledOverride(color);
+		_femaleRadio->setUntoggledOverride(color);
+	}
 }
 
 } // namespace
@@ -862,6 +930,14 @@ int PanelDetailsRow::resizeGetHeight(int newWidth) {
 }
 
 void PanelDetailsRow::showError(const QString &error) {
+	if (!_errorHideSubscription) {
+		_errorHideSubscription = true;
+
+		value(
+		) | rpl::start_with_next([=] {
+			hideError();
+		}, lifetime());
+	}
 	showInnerError();
 	startErrorAnimation(true);
 	if (!error.isEmpty()) {
@@ -873,10 +949,6 @@ void PanelDetailsRow::showError(const QString &error) {
 					error,
 					Ui::FlatLabel::InitType::Simple,
 					st::passportVerifyErrorLabel));
-			value(
-			) | rpl::start_with_next([=] {
-				hideError();
-			}, lifetime());
 		} else {
 			_error->entity()->setText(error);
 		}
diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.h b/Telegram/SourceFiles/passport/passport_panel_details_row.h
index bd4ff6283..b6216b693 100644
--- a/Telegram/SourceFiles/passport/passport_panel_details_row.h
+++ b/Telegram/SourceFiles/passport/passport_panel_details_row.h
@@ -83,6 +83,7 @@ private:
 	QString _label;
 	object_ptr<Ui::SlideWrap<Ui::FlatLabel>> _error = { nullptr };
 	bool _errorShown = false;
+	bool _errorHideSubscription = false;
 	Animation _errorAnimation;
 
 };
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
index c909ae8fe..47e888552 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
@@ -299,7 +299,29 @@ PanelEditDocument::Result PanelEditDocument::collect() const {
 	return result;
 }
 
+bool PanelEditDocument::validate() {
+	auto first = QPointer<PanelDetailsRow>();
+	for (const auto [i, field] : base::reversed(_details)) {
+		const auto &row = _scheme.rows[i];
+		if (row.validate && !row.validate(field->valueCurrent())) {
+			field->showError(QString());
+			first = field;
+		}
+	}
+	if (!first) {
+		return true;
+	}
+	const auto firsttop = first->mapToGlobal(QPoint(0, 0));
+	const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
+	const auto scrolldelta = firsttop.y() - scrolltop.y();
+	_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
+	return false;
+}
+
 void PanelEditDocument::save() {
+	if (!validate()) {
+		return;
+	}
 	auto result = collect();
 	_controller->saveScope(
 		std::move(result.data),
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h
index 0ff4f1c42..693db926d 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h
@@ -84,6 +84,7 @@ private:
 	void updateControlsGeometry();
 
 	Result collect() const;
+	bool validate();
 	void save();
 
 	not_null<PanelController*> _controller;
diff --git a/Telegram/SourceFiles/ui/widgets/checkbox.cpp b/Telegram/SourceFiles/ui/widgets/checkbox.cpp
index 9c6adf3da..d1fc354d6 100644
--- a/Telegram/SourceFiles/ui/widgets/checkbox.cpp
+++ b/Telegram/SourceFiles/ui/widgets/checkbox.cpp
@@ -43,6 +43,12 @@ void AbstractCheckView::setUpdateCallback(base::lambda<void()> updateCallback) {
 	}
 }
 
+void AbstractCheckView::update() {
+	if (_updateCallback) {
+		_updateCallback();
+	}
+}
+
 void AbstractCheckView::setCheckedAnimated(bool checked) {
 	if (_checked != checked) {
 		_checked = checked;
@@ -212,10 +218,17 @@ void CheckView::setStyle(const style::Check &st) {
 
 void CheckView::paint(Painter &p, int left, int top, int outerWidth, TimeMs ms) {
 	auto toggled = currentAnimationValue(ms);
-	auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, toggled);
+	auto pen = _untoggledOverride
+		? anim::pen(*_untoggledOverride, _st->toggledFg, toggled)
+		: anim::pen(_st->untoggledFg, _st->toggledFg, toggled);
 	pen.setWidth(_st->thickness);
 	p.setPen(pen);
-	p.setBrush(anim::brush(_st->bg, anim::color(_st->untoggledFg, _st->toggledFg, toggled), toggled));
+	p.setBrush(anim::brush(
+		_st->bg,
+		(_untoggledOverride
+			? anim::color(*_untoggledOverride, _st->toggledFg, toggled)
+			: anim::color(_st->untoggledFg, _st->toggledFg, toggled)),
+		toggled));
 
 	{
 		PainterHighQualityEnabler hq(p);
@@ -239,7 +252,17 @@ bool CheckView::checkRippleStartPosition(QPoint position) const {
 	return QRect(QPoint(0, 0), rippleSize()).contains(position);
 }
 
-RadioView::RadioView(const style::Radio &st, bool checked, base::lambda<void()> updateCallback) : AbstractCheckView(st.duration, checked, std::move(updateCallback))
+void CheckView::setUntoggledOverride(
+		base::optional<QColor> untoggledOverride) {
+	_untoggledOverride = untoggledOverride;
+	update();
+}
+
+RadioView::RadioView(
+	const style::Radio &st,
+	bool checked,
+	base::lambda<void()> updateCallback)
+: AbstractCheckView(st.duration, checked, std::move(updateCallback))
 , _st(&st) {
 }
 
@@ -255,7 +278,9 @@ void RadioView::paint(Painter &p, int left, int top, int outerWidth, TimeMs ms)
 	PainterHighQualityEnabler hq(p);
 
 	auto toggled = currentAnimationValue(ms);
-	auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, toggled);
+	auto pen = _untoggledOverride
+		? anim::pen(*_untoggledOverride, _st->toggledFg, toggled)
+		: anim::pen(_st->untoggledFg, _st->toggledFg, toggled);
 	pen.setWidth(_st->thickness);
 	p.setPen(pen);
 	p.setBrush(_st->bg);
@@ -265,7 +290,9 @@ void RadioView::paint(Painter &p, int left, int top, int outerWidth, TimeMs ms)
 
 	if (toggled > 0) {
 		p.setPen(Qt::NoPen);
-		p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled));
+		p.setBrush(_untoggledOverride
+			? anim::brush(*_untoggledOverride, _st->toggledFg, toggled)
+			: anim::brush(_st->untoggledFg, _st->toggledFg, toggled));
 
 		auto skip0 = _st->diameter / 2., skip1 = _st->skip / 10., checkSkip = skip0 * (1. - toggled) + skip1 * toggled;
 		p.drawEllipse(rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(checkSkip, checkSkip, checkSkip, checkSkip)), outerWidth));
@@ -295,16 +322,52 @@ bool RadioView::checkRippleStartPosition(QPoint position) const {
 	return QRect(QPoint(0, 0), rippleSize()).contains(position);
 }
 
-Checkbox::Checkbox(QWidget *parent, const QString &text, bool checked, const style::Checkbox &st, const style::Check &checkSt) : Checkbox(parent, text, st, std::make_unique<CheckView>(checkSt, checked, [this] { updateCheck(); })) {
+void RadioView::setUntoggledOverride(
+		base::optional<QColor> untoggledOverride) {
+	_untoggledOverride = untoggledOverride;
+	update();
 }
 
-Checkbox::Checkbox(QWidget *parent, const QString &text, bool checked, const style::Checkbox &st, const style::Toggle &toggleSt) : Checkbox(parent, text, st, std::make_unique<ToggleView>(toggleSt, checked, [this] { updateCheck(); })) {
+Checkbox::Checkbox(
+	QWidget *parent,
+	const QString &text,
+	bool checked,
+	const style::Checkbox &st,
+	const style::Check &checkSt)
+: Checkbox(
+	parent,
+	text,
+	st,
+	std::make_unique<CheckView>(
+		checkSt,
+		checked)) {
 }
 
-Checkbox::Checkbox(QWidget *parent, const QString &text, const style::Checkbox &st, std::unique_ptr<AbstractCheckView> check) : RippleButton(parent, st.ripple)
+Checkbox::Checkbox(
+	QWidget *parent,
+	const QString &text,
+	bool checked,
+	const style::Checkbox &st,
+	const style::Toggle &toggleSt)
+: Checkbox(
+	parent,
+	text,
+	st,
+	std::make_unique<ToggleView>(
+		toggleSt,
+		checked)) {
+}
+
+Checkbox::Checkbox(
+	QWidget *parent,
+	const QString &text,
+	const style::Checkbox &st,
+	std::unique_ptr<AbstractCheckView> check)
+: RippleButton(parent, st.ripple)
 , _st(st)
 , _check(std::move(check))
 , _text(_st.style, text, _checkboxOptions) {
+	_check->setUpdateCallback([=] { updateCheck(); });
 	resizeToText();
 	setCursor(style::cur_pointer);
 }
@@ -502,9 +565,39 @@ void RadiobuttonGroup::setValue(int value) {
 	}
 }
 
-Radiobutton::Radiobutton(QWidget *parent, const std::shared_ptr<RadiobuttonGroup> &group, int value, const QString &text, const style::Checkbox &st, const style::Radio &radioSt) : Checkbox(parent, text, st, std::make_unique<RadioView>(radioSt, (group->hasValue() && group->value() == value), [this] { updateCheck(); }))
+Radiobutton::Radiobutton(
+	QWidget *parent,
+	const std::shared_ptr<RadiobuttonGroup> &group,
+	int value,
+	const QString &text,
+	const style::Checkbox &st,
+	const style::Radio &radioSt)
+: Radiobutton(
+	parent,
+	group,
+	value,
+	text,
+	st,
+	std::make_unique<RadioView>(
+		radioSt,
+		(group->hasValue() && group->value() == value))) {
+}
+
+Radiobutton::Radiobutton(
+	QWidget *parent,
+	const std::shared_ptr<RadiobuttonGroup> &group,
+	int value,
+	const QString &text,
+	const style::Checkbox &st,
+	std::unique_ptr<AbstractCheckView> check)
+: Checkbox(
+	parent,
+	text,
+	st,
+	std::move(check))
 , _group(group)
 , _value(value) {
+	checkbox()->setChecked(group->hasValue() && group->value() == value);
 	_group->registerButton(this);
 	subscribe(checkbox()->checkedChanged, [this](bool checked) {
 		if (checked) {
diff --git a/Telegram/SourceFiles/ui/widgets/checkbox.h b/Telegram/SourceFiles/ui/widgets/checkbox.h
index f24a27160..c5fc35cfb 100644
--- a/Telegram/SourceFiles/ui/widgets/checkbox.h
+++ b/Telegram/SourceFiles/ui/widgets/checkbox.h
@@ -24,6 +24,7 @@ public:
 	bool checked() const {
 		return _checked;
 	}
+	void update();
 	float64 currentAnimationValue(TimeMs ms);
 
 	auto checkedValue() const {
@@ -57,7 +58,10 @@ private:
 
 class CheckView : public AbstractCheckView {
 public:
-	CheckView(const style::Check &st, bool checked, base::lambda<void()> updateCallback);
+	CheckView(
+		const style::Check &st,
+		bool checked,
+		base::lambda<void()> updateCallback = nullptr);
 
 	void setStyle(const style::Check &st);
 
@@ -66,19 +70,28 @@ public:
 	QImage prepareRippleMask() const override;
 	bool checkRippleStartPosition(QPoint position) const override;
 
+	void setUntoggledOverride(
+		base::optional<QColor> untoggledOverride);
+
 private:
 	QSize rippleSize() const;
 
 	not_null<const style::Check*> _st;
+	base::optional<QColor> _untoggledOverride;
 
 };
 
 class RadioView : public AbstractCheckView {
 public:
-	RadioView(const style::Radio &st, bool checked, base::lambda<void()> updateCallback);
+	RadioView(
+		const style::Radio &st,
+		bool checked,
+		base::lambda<void()> updateCallback = nullptr);
 
 	void setStyle(const style::Radio &st);
 
+	void setUntoggledOverride(base::optional<QColor> untoggledOverride);
+
 	QSize getSize() const override;
 	void paint(Painter &p, int left, int top, int outerWidth, TimeMs ms) override;
 	QImage prepareRippleMask() const override;
@@ -88,12 +101,16 @@ private:
 	QSize rippleSize() const;
 
 	not_null<const style::Radio*> _st;
+	base::optional<QColor> _untoggledOverride;
 
 };
 
 class ToggleView : public AbstractCheckView {
 public:
-	ToggleView(const style::Toggle &st, bool checked, base::lambda<void()> updateCallback);
+	ToggleView(
+		const style::Toggle &st,
+		bool checked,
+		base::lambda<void()> updateCallback = nullptr);
 
 	void setStyle(const style::Toggle &st);
 
@@ -112,9 +129,23 @@ private:
 
 class Checkbox : public RippleButton {
 public:
-	Checkbox(QWidget *parent, const QString &text, bool checked = false, const style::Checkbox &st = st::defaultCheckbox, const style::Check &checkSt = st::defaultCheck);
-	Checkbox(QWidget *parent, const QString &text, bool checked, const style::Checkbox &st, const style::Toggle &toggleSt);
-	Checkbox(QWidget *parent, const QString &text, const style::Checkbox &st, std::unique_ptr<AbstractCheckView> check);
+	Checkbox(
+		QWidget *parent,
+		const QString &text,
+		bool checked = false,
+		const style::Checkbox &st = st::defaultCheckbox,
+		const style::Check &checkSt = st::defaultCheck);
+	Checkbox(
+		QWidget *parent,
+		const QString &text,
+		bool checked,
+		const style::Checkbox &st,
+		const style::Toggle &toggleSt);
+	Checkbox(
+		QWidget *parent,
+		const QString &text,
+		const style::Checkbox &st,
+		std::unique_ptr<AbstractCheckView> check);
 
 	void setText(const QString &text);
 	void setCheckAlignment(style::align alignment);
@@ -203,7 +234,20 @@ private:
 
 class Radiobutton : public Checkbox, private base::Subscriber {
 public:
-	Radiobutton(QWidget *parent, const std::shared_ptr<RadiobuttonGroup> &group, int value, const QString &text, const style::Checkbox &st = st::defaultCheckbox, const style::Radio &radioSt = st::defaultRadio);
+	Radiobutton(
+		QWidget *parent,
+		const std::shared_ptr<RadiobuttonGroup> &group,
+		int value,
+		const QString &text,
+		const style::Checkbox &st = st::defaultCheckbox,
+		const style::Radio &radioSt = st::defaultRadio);
+	Radiobutton(
+		QWidget *parent,
+		const std::shared_ptr<RadiobuttonGroup> &group,
+		int value,
+		const QString &text,
+		const style::Checkbox &st,
+		std::unique_ptr<AbstractCheckView> check);
 	~Radiobutton();
 
 protected:
@@ -267,8 +311,33 @@ private:
 template <typename Enum>
 class Radioenum : public Radiobutton {
 public:
-	Radioenum(QWidget *parent, const std::shared_ptr<RadioenumGroup<Enum>> &group, Enum value, const QString &text, const style::Checkbox &st = st::defaultCheckbox)
-		: Radiobutton(parent, std::shared_ptr<RadiobuttonGroup>(group, &group->_group), static_cast<int>(value), text, st) {
+	Radioenum(
+		QWidget *parent,
+		const std::shared_ptr<RadioenumGroup<Enum>> &group,
+		Enum value,
+		const QString &text,
+		const style::Checkbox &st = st::defaultCheckbox)
+	: Radiobutton(
+		parent,
+		std::shared_ptr<RadiobuttonGroup>(group, &group->_group),
+		static_cast<int>(value),
+		text,
+		st) {
+	}
+	Radioenum(
+		QWidget *parent,
+		const std::shared_ptr<RadioenumGroup<Enum>> &group,
+		Enum value,
+		const QString &text,
+		const style::Checkbox &st,
+		std::unique_ptr<AbstractCheckView> check)
+		: Radiobutton(
+			parent,
+			std::shared_ptr<RadiobuttonGroup>(group, &group->_group),
+			static_cast<int>(value),
+			text,
+			st,
+			std::move(check)) {
 	}
 
 };