From 3c43f621cebf699452b739a28e81c8935ebe9086 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sun, 12 Aug 2018 22:48:43 +0300
Subject: [PATCH] Start improved passport support.

---
 Telegram/Resources/langs/lang.strings         |   1 -
 Telegram/Resources/scheme.tl                  |  14 +-
 .../passport/passport_form_controller.cpp     | 174 +++++++++----
 .../passport/passport_form_controller.h       |  33 ++-
 .../passport_form_view_controller.cpp         | 241 +++++++++++++-----
 .../passport/passport_form_view_controller.h  |  11 +-
 .../passport/passport_panel_controller.cpp    | 137 +++++-----
 7 files changed, 416 insertions(+), 195 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 86e39de2b..5bf6b6bf5 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1668,7 +1668,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_passport_error_cant_read" = "Can't read this file. Please choose an image.";
 "lng_passport_bad_name" = "Please use latin characters only.";
 "lng_passport_wait_upload" = "Please wait while upload is finished.";
-"lng_passport_fix_errors" = "Please correct errors.";
 "lng_passport_app_out_of_date" = "Sorry, your Telegram app is out of date and can't handle this request. Please update Telegram.";
 
 "lng_export_title" = "Export Personal Data";
diff --git a/Telegram/Resources/scheme.tl b/Telegram/Resources/scheme.tl
index 587d753cd..7125ec24a 100644
--- a/Telegram/Resources/scheme.tl
+++ b/Telegram/Resources/scheme.tl
@@ -995,9 +995,9 @@ secureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType;
 secureValueTypePhone#b320aadb = SecureValueType;
 secureValueTypeEmail#8e3ca7ee = SecureValueType;
 
-secureValue#b4b4b699 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile files:flags.4?Vector<SecureFile> plain_data:flags.5?SecurePlainData hash:bytes = SecureValue;
+secureValue#187fa0ca flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile translation:flags.6?Vector<SecureFile> files:flags.4?Vector<SecureFile> plain_data:flags.5?SecurePlainData hash:bytes = SecureValue;
 
-inputSecureValue#67872e8 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile files:flags.4?Vector<InputSecureFile> plain_data:flags.5?SecurePlainData = InputSecureValue;
+inputSecureValue#db21d0a7 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile translation:flags.6?Vector<InputSecureFile> files:flags.4?Vector<InputSecureFile> plain_data:flags.5?SecurePlainData = InputSecureValue;
 
 secureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash;
 
@@ -1007,10 +1007,13 @@ secureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:s
 secureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError;
 secureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError;
 secureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;
+secureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError;
+secureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError;
+secureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;
 
 secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted;
 
-account.authorizationForm#cb976d53 flags:# selfie_required:flags.1?true required_types:Vector<SecureValueType> values:Vector<SecureValue> errors:Vector<SecureValueError> users:Vector<User> privacy_policy_url:flags.0?string = account.AuthorizationForm;
+account.authorizationForm#ad2e1cd8 flags:# required_types:Vector<SecureRequiredType> values:Vector<SecureValue> errors:Vector<SecureValueError> users:Vector<User> privacy_policy_url:flags.0?string = account.AuthorizationForm;
 
 account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode;
 
@@ -1033,6 +1036,9 @@ secureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:by
 inputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP;
 inputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP;
 
+secureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType;
+secureRequiredTypeOneOf#27477b4 types:Vector<SecureRequiredType> = SecureRequiredType;
+
 ---functions---
 
 invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@@ -1321,4 +1327,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangP
 langpack.getDifference#b2e4d7d from_version:int = LangPackDifference;
 langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
 
-// LAYER 84
+// LAYER 85
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp
index 8624ce8ef..72ea2be1a 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp
@@ -132,7 +132,30 @@ MTPSecureValueType ConvertType(Value::Type type) {
 		return MTP_secureValueTypeEmail();
 	}
 	Unexpected("Type in FormController::submit.");
-};
+}
+
+void CollectToRequestedRow(
+		RequestedRow &row,
+		const MTPSecureRequiredType &data) {
+	data.match([&](const MTPDsecureRequiredType &data) {
+		row.values.emplace_back(ConvertType(data.vtype));
+		auto &value = row.values.back();
+		value.selfieRequired = data.is_selfie_required();
+		value.translationRequired = data.is_translation_required();
+		value.nativeNames = data.is_native_names();
+	}, [&](const MTPDsecureRequiredTypeOneOf &data) {
+		row.values.reserve(row.values.size() + data.vtypes.v.size());
+		for (const auto &one : data.vtypes.v) {
+			CollectToRequestedRow(row, one);
+		}
+	});
+}
+
+RequestedRow CollectRequestedRow(const MTPSecureRequiredType &data) {
+	auto result = RequestedRow();
+	CollectToRequestedRow(result, data);
+	return result;
+}
 
 QJsonObject GetJSONFromMap(
 	const std::map<QString, bytes::const_span> &map) {
@@ -257,12 +280,13 @@ UploadScanData *UploadScanDataPointer::operator->() const {
 	return _value.get();
 }
 
+RequestedValue::RequestedValue(Value::Type type) : type(type) {
+}
+
 Value::Value(Type type) : type(type) {
 }
 
-bool Value::requiresSpecialScan(
-		SpecialFile type,
-		bool selfieRequired) const {
+bool Value::requiresSpecialScan(SpecialFile type) const {
 	switch (type) {
 	case SpecialFile::FrontSide:
 		return (this->type == Type::Passport)
@@ -278,9 +302,11 @@ bool Value::requiresSpecialScan(
 	Unexpected("Special scan type in requiresSpecialScan.");
 }
 
-bool Value::scansAreFilled(bool selfieRequired) const {
-	if (!requiresSpecialScan(SpecialFile::FrontSide, selfieRequired)) {
-		return !scans.empty();
+bool Value::scansAreFilled() const {
+	if (!requiresSpecialScan(SpecialFile::FrontSide) && scans.empty()) {
+		return false;
+	} else if (translationRequired && translations.empty()) {
+		return false;
 	}
 	const auto types = {
 		SpecialFile::FrontSide,
@@ -288,14 +314,13 @@ bool Value::scansAreFilled(bool selfieRequired) const {
 		SpecialFile::Selfie
 	};
 	for (const auto type : types) {
-		if (requiresSpecialScan(type, selfieRequired)
+		if (requiresSpecialScan(type)
 			&& (specialScans.find(type) == end(specialScans))) {
 			return false;
 		}
 	}
 	return true;
-};
-
+}
 
 FormController::FormController(
 	not_null<Window::Controller*> controller,
@@ -346,8 +371,7 @@ auto FormController::prepareFinalData() -> FinalData {
 			object.insert("files", files);
 		}
 		for (const auto &[type, scan] : value->specialScans) {
-			const auto selfieRequired = _form.identitySelfieRequired;
-			if (value->requiresSpecialScan(type, selfieRequired)) {
+			if (value->requiresSpecialScan(type)) {
 				object.insert(
 					SpecialScanCredentialsKey(type),
 					GetJSONFromFile(scan));
@@ -364,17 +388,21 @@ auto FormController::prepareFinalData() -> FinalData {
 			addValueToJSON(key, value);
 		}
 	};
-	const auto scopes = ComputeScopes(this);
+	const auto scopes = ComputeScopes(_form);
 	for (const auto &scope : scopes) {
 		const auto row = ComputeScopeRow(scope);
 		if (row.ready.isEmpty() || !row.error.isEmpty()) {
-			errors.push_back(scope.fields);
+			errors.push_back(scope.details
+				? scope.details
+				: scope.documents[0].get());
 			continue;
 		}
-		addValue(scope.fields);
+		if (scope.details) {
+			addValue(scope.details);
+		}
 		if (!scope.documents.empty()) {
 			for (const auto &document : scope.documents) {
-				if (document->scansAreFilled(scope.selfieRequired)) {
+				if (document->scansAreFilled()) {
 					addValue(document);
 					break;
 				}
@@ -803,6 +831,7 @@ void FormController::decryptValues() {
 }
 
 void FormController::fillErrors() {
+	// #TODO passport filter by flags
 	const auto find = [&](const MTPSecureValueType &type) -> Value* {
 		const auto converted = ConvertType(type);
 		const auto i = _form.values.find(ConvertType(type));
@@ -835,43 +864,44 @@ void FormController::fillErrors() {
 		}
 	};
 	for (const auto &error : _form.pendingErrors) {
-		switch (error.type()) {
-		case mtpc_secureValueErrorData: {
-			const auto &data = error.c_secureValueErrorData();
+		error.match([&](const MTPDsecureValueError &data) {
+			if (const auto value = find(data.vtype)) {
+				value->error = qs(data.vtext);
+			}
+		}, [&](const MTPDsecureValueErrorData &data) {
 			if (const auto value = find(data.vtype)) {
 				const auto key = qs(data.vfield);
 				value->data.parsed.fields[key].error = qs(data.vtext);
 			}
-		} break;
-		case mtpc_secureValueErrorFile: {
-			const auto &data = error.c_secureValueErrorFile();
+		}, [&](const MTPDsecureValueErrorFile &data) {
 			const auto hash = bytes::make_span(data.vfile_hash.v);
 			if (const auto value = find(data.vtype)) {
 				if (const auto file = scan(*value, hash)) {
 					file->error = qs(data.vtext);
 				}
 			}
-		} break;
-		case mtpc_secureValueErrorFiles: {
-			const auto &data = error.c_secureValueErrorFiles();
+		}, [&](const MTPDsecureValueErrorFiles &data) {
 			if (const auto value = find(data.vtype)) {
 				value->scanMissingError = qs(data.vtext);
 			}
-		} break;
-		case mtpc_secureValueErrorFrontSide: {
-			const auto &data = error.c_secureValueErrorFrontSide();
+		}, [&](const MTPDsecureValueErrorTranslationFile &data) {
+			const auto hash = bytes::make_span(data.vfile_hash.v);
+			if (const auto value = find(data.vtype)) {
+				if (const auto file = scan(*value, hash)) { // #TODO passport
+					file->error = qs(data.vtext);
+				}
+			}
+		}, [&](const MTPDsecureValueErrorTranslationFiles &data) {
+			if (const auto value = find(data.vtype)) {
+				value->translationMissingError = qs(data.vtext);
+			}
+		}, [&](const MTPDsecureValueErrorFrontSide &data) {
 			setSpecialScanError(SpecialFile::FrontSide, data);
-		} break;
-		case mtpc_secureValueErrorReverseSide: {
-			const auto &data = error.c_secureValueErrorReverseSide();
+		}, [&](const MTPDsecureValueErrorReverseSide &data) {
 			setSpecialScanError(SpecialFile::ReverseSide, data);
-		} break;
-		case mtpc_secureValueErrorSelfie: {
-			const auto &data = error.c_secureValueErrorSelfie();
+		}, [&](const MTPDsecureValueErrorSelfie &data) {
 			setSpecialScanError(SpecialFile::Selfie, data);
-		} break;
-		default: Unexpected("Error type in FormController::fillErrors.");
-		}
+		});
 	}
 }
 
@@ -1312,9 +1342,13 @@ void FormController::startValueEdit(not_null<const Value*> value) {
 	for (auto &scan : nonconst->scans) {
 		loadFile(scan);
 	}
+	if (nonconst->translationRequired) {
+		for (auto &scan : nonconst->translations) {
+			loadFile(scan);
+		}
+	}
 	for (auto &[type, scan] : nonconst->specialScans) {
-		const auto selfieRequired = _form.identitySelfieRequired;
-		if (nonconst->requiresSpecialScan(type, selfieRequired)) {
+		if (nonconst->requiresSpecialScan(type)) {
 			loadFile(scan);
 		}
 	}
@@ -1324,6 +1358,12 @@ void FormController::startValueEdit(not_null<const Value*> value) {
 		return EditFile(nonconst, file, nullptr);
 	}) | ranges::to_vector;
 
+	nonconst->translationsInEdit = ranges::view::all(
+		nonconst->translations
+	) | ranges::view::transform([=](const File &file) {
+		return EditFile(nonconst, file, nullptr);
+	}) | ranges::to_vector;
+
 	nonconst->specialScansInEdit.clear();
 	for (const auto &[type, scan] : nonconst->specialScans) {
 		nonconst->specialScansInEdit.emplace(type, EditFile(
@@ -1597,7 +1637,7 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 		return;
 	}
 
-	const auto inputFile = [](const EditFile &file) {
+	const auto wrapFile = [](const EditFile &file) {
 		if (const auto uploadData = file.uploadData.get()) {
 			return MTP_inputSecureFileUploaded(
 				MTP_long(file.fields.id),
@@ -1611,13 +1651,22 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 			MTP_long(file.fields.accessHash));
 	};
 
-	auto inputFiles = QVector<MTPInputSecureFile>();
-	inputFiles.reserve(value->scansInEdit.size());
+	auto files = QVector<MTPInputSecureFile>();
+	files.reserve(value->scansInEdit.size());
 	for (const auto &scan : value->scansInEdit) {
 		if (scan.deleted) {
 			continue;
 		}
-		inputFiles.push_back(inputFile(scan));
+		files.push_back(wrapFile(scan));
+	}
+
+	auto translations = QVector<MTPInputSecureFile>();
+	translations.reserve(value->translationsInEdit.size());
+	for (const auto &scan : value->translationsInEdit) {
+		if (scan.deleted) {
+			continue;
+		}
+		translations.push_back(wrapFile(scan));
 	}
 
 	if (value->data.secret.empty()) {
@@ -1639,7 +1688,7 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 	const auto specialFile = [&](SpecialFile type) {
 		const auto i = value->specialScansInEdit.find(type);
 		return (i != end(value->specialScansInEdit) && !i->second.deleted)
-			? inputFile(i->second)
+			? wrapFile(i->second)
 			: MTPInputSecureFile();
 	};
 	const auto frontSide = specialFile(SpecialFile::FrontSide);
@@ -1659,6 +1708,9 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 		| (hasSpecialFile(SpecialFile::Selfie)
 			? MTPDinputSecureValue::Flag::f_selfie
 			: MTPDinputSecureValue::Flag(0))
+		| (value->translationsInEdit.empty()
+			? MTPDinputSecureValue::Flag(0)
+			: MTPDinputSecureValue::Flag::f_translation)
 		| (value->scansInEdit.empty()
 			? MTPDinputSecureValue::Flag(0)
 			: MTPDinputSecureValue::Flag::f_files);
@@ -1674,7 +1726,8 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 		frontSide,
 		reverseSide,
 		selfie,
-		MTP_vector<MTPInputSecureFile>(inputFiles),
+		MTP_vector<MTPInputSecureFile>(translations),
+		MTP_vector<MTPInputSecureFile>(files),
 		MTPSecurePlainData()));
 }
 
@@ -1704,6 +1757,7 @@ void FormController::savePlainTextValue(not_null<Value*> value) {
 		MTPInputSecureFile(),
 		MTPInputSecureFile(),
 		MTPVector<MTPInputSecureFile>(),
+		MTPVector<MTPInputSecureFile>(),
 		plain(MTP_string(text))));
 }
 
@@ -2157,13 +2211,14 @@ auto FormController::findFile(const FileKey &key)
 }
 
 void FormController::formDone(const MTPaccount_AuthorizationForm &result) {
-	parseForm(result);
-	if (!_passwordRequestId) {
+	if (!parseForm(result)) {
+		_view->showCriticalError(lang(lng_passport_form_error));
+	} else if (!_passwordRequestId) {
 		showForm();
 	}
 }
 
-void FormController::parseForm(const MTPaccount_AuthorizationForm &result) {
+bool FormController::parseForm(const MTPaccount_AuthorizationForm &result) {
 	Expects(result.type() == mtpc_account_authorizationForm);
 
 	const auto &data = result.c_account_authorizationForm();
@@ -2177,21 +2232,34 @@ void FormController::parseForm(const MTPaccount_AuthorizationForm &result) {
 		if (alreadyIt != _form.values.end()) {
 			LOG(("API Error: Two values for type %1 in authorization form"
 				"%1").arg(int(type)));
-			continue;
+			return false;
 		}
 		_form.values.emplace(type, std::move(parsed));
 	}
-	_form.identitySelfieRequired = data.is_selfie_required();
 	if (data.has_privacy_policy_url()) {
 		_form.privacyPolicyUrl = qs(data.vprivacy_policy_url);
 	}
 	for (const auto &required : data.vrequired_types.v) {
-		const auto type = ConvertType(required);
-		_form.request.push_back(type);
-		_form.values.emplace(type, Value(type));
+		const auto row = CollectRequestedRow(required);
+		for (const auto value : row.values) {
+			const auto [i, ok] = _form.values.emplace(
+				value.type,
+				Value(value.type));
+			i->second.selfieRequired = value.selfieRequired;
+			i->second.translationRequired = value.translationRequired;
+			i->second.nativeNames = value.nativeNames;
+		}
+		_form.request.push_back(row.values
+			| ranges::view::transform([](const RequestedValue &value) {
+				return value.type;
+			}) | ranges::to_vector);
+	}
+	if (!ValidateForm(_form)) {
+		return false;
 	}
 	_bot = App::userLoaded(_request.botId);
 	_form.pendingErrors = data.verrors.v;
+	return true;
 }
 
 void FormController::formFail(const QString &error) {
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h
index a709456a9..32ff2fae8 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_controller.h
@@ -168,31 +168,52 @@ struct Value {
 	Value(Value &&other) = default;
 	Value &operator=(Value &&other) = default;
 
-	bool requiresSpecialScan(SpecialFile type, bool selfieRequired) const;
-	bool scansAreFilled(bool selfieRequired) const;
+	bool requiresSpecialScan(SpecialFile type) const;
+	bool scansAreFilled() const;
 
 	Type type;
 	ValueData data;
 	std::vector<File> scans;
+	std::vector<File> translations;
 	std::map<SpecialFile, File> specialScans;
+	QString error;
 	QString scanMissingError;
+	QString translationMissingError;
 	std::vector<EditFile> scansInEdit;
+	std::vector<EditFile> translationsInEdit;
 	std::map<SpecialFile, EditFile> specialScansInEdit;
 	Verification verification;
 	bytes::vector submitHash;
 
+	bool selfieRequired = false;
+	bool translationRequired = false;
+	bool nativeNames = false;
+
 	int editScreens = 0;
 	mtpRequestId saveRequestId = 0;
 
 };
 
+struct RequestedValue {
+	explicit RequestedValue(Value::Type type);
+
+	Value::Type type;
+	bool selfieRequired = false;
+	bool translationRequired = false;
+	bool nativeNames = false;
+};
+
+struct RequestedRow {
+	std::vector<RequestedValue> values;
+};
+
 struct Form {
+	using Request = std::vector<std::vector<Value::Type>>;
+
 	std::map<Value::Type, Value> values;
-	std::vector<Value::Type> request;
-	bool identitySelfieRequired = false;
+	Request request;
 	QString privacyPolicyUrl;
 	QVector<MTPSecureValueError> pendingErrors;
-
 };
 
 struct PasswordSettings {
@@ -333,7 +354,7 @@ private:
 
 	void formDone(const MTPaccount_AuthorizationForm &result);
 	void formFail(const QString &error);
-	void parseForm(const MTPaccount_AuthorizationForm &result);
+	bool parseForm(const MTPaccount_AuthorizationForm &result);
 	void showForm();
 	Value parseValue(
 		const MTPSecureValue &value,
diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
index d83ca197b..71c005704 100644
--- a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
@@ -18,12 +18,12 @@ namespace {
 
 std::map<Value::Type, Scope::Type> ScopeTypesMap() {
 	return {
-		{ Value::Type::PersonalDetails, Scope::Type::Identity },
+		{ Value::Type::PersonalDetails, Scope::Type::PersonalDetails },
 		{ Value::Type::Passport, Scope::Type::Identity },
 		{ Value::Type::DriverLicense, Scope::Type::Identity },
 		{ Value::Type::IdentityCard, Scope::Type::Identity },
 		{ Value::Type::InternalPassport, Scope::Type::Identity },
-		{ Value::Type::Address, Scope::Type::Address },
+		{ Value::Type::Address, Scope::Type::AddressDetails },
 		{ Value::Type::UtilityBill, Scope::Type::Address },
 		{ Value::Type::BankStatement, Scope::Type::Address },
 		{ Value::Type::RentalAgreement, Scope::Type::Address },
@@ -41,62 +41,141 @@ Scope::Type ScopeTypeForValueType(Value::Type type) {
 	return i->second;
 }
 
-std::map<Scope::Type, Value::Type> ScopeFieldsMap() {
+std::map<Scope::Type, Value::Type> ScopeDetailsMap() {
 	return {
+		{ Scope::Type::PersonalDetails, Value::Type::PersonalDetails },
 		{ Scope::Type::Identity, Value::Type::PersonalDetails },
+		{ Scope::Type::AddressDetails, Value::Type::Address },
 		{ Scope::Type::Address, Value::Type::Address },
 		{ Scope::Type::Phone, Value::Type::Phone },
 		{ Scope::Type::Email, Value::Type::Email },
 	};
 }
 
-Value::Type FieldsTypeForScopeType(Scope::Type type) {
-	static const auto map = ScopeFieldsMap();
+Value::Type DetailsTypeForScopeType(Scope::Type type) {
+	static const auto map = ScopeDetailsMap();
 	const auto i = map.find(type);
 	Assert(i != map.end());
 	return i->second;
 }
 
-} // namespace
+bool InlineDetails(
+		const Form::Request &request,
+		Scope::Type into,
+		Value::Type details) {
+	const auto count = ranges::count_if(
+		request,
+		[&](const std::vector<Value::Type> &types) {
+			Expects(!types.empty());
 
-Scope::Scope(Type type, not_null<const Value*> fields)
-: type(type)
-, fields(fields) {
+			return ScopeTypeForValueType(types[0]) == into;
+		});
+	if (count != 1) {
+		return false;
+	}
+	const auto has = ranges::find_if(
+		request,
+		[&](const std::vector<Value::Type> &types) {
+			Expects(!types.empty());
+
+			return (types[0] == details);
+		}
+	) != end(request);
+	return has;
 }
 
-std::vector<Scope> ComputeScopes(
-		not_null<const FormController*> controller) {
-	auto scopes = std::map<Scope::Type, Scope>();
-	const auto &form = controller->form();
+bool InlineDetails(const Form::Request &request, Value::Type details) {
+	if (details == Value::Type::PersonalDetails) {
+		return InlineDetails(request, Scope::Type::Identity, details);
+	} else if (details == Value::Type::Address) {
+		return InlineDetails(request, Scope::Type::Address, details);
+	}
+	return false;
+}
+
+} // namespace
+
+Scope::Scope(Type type) : type(type) {
+}
+
+bool ValidateForm(const Form &form) {
+	base::flat_set<Value::Type> values;
+	for (const auto &requested : form.request) {
+		if (requested.empty()) {
+			LOG(("API Error: Empty types list in authorization form row."));
+			return false;
+		}
+		const auto scopeType = ScopeTypeForValueType(requested[0]);
+		const auto ownsDetails = (scopeType != Scope::Type::Identity
+			&& scopeType != Scope::Type::Address);
+		if (ownsDetails && requested.size() != 1) {
+			LOG(("API Error: Large types list in authorization form row."));
+			return false;
+		}
+		for (const auto type : requested) {
+			if (values.contains(type)) {
+				LOG(("API Error: Value twice in authorization form row."));
+				return false;
+			}
+			values.emplace(type);
+		}
+	}
+	for (const auto &[type, value] : form.values) {
+		if (!value.translationRequired) {
+			for (const auto &scan : value.translations) {
+				if (!scan.error.isEmpty()) {
+					LOG(("API Error: "
+						"Translation error in authorization form value."));
+					return false;
+				}
+			}
+			if (!value.translationMissingError.isEmpty()) {
+				LOG(("API Error: "
+					"Translations error in authorization form value."));
+				return false;
+			}
+		}
+		for (const auto &[type, specialScan] : value.specialScans) {
+			if (!value.requiresSpecialScan(type)
+				&& !specialScan.error.isEmpty()) {
+				LOG(("API Error: "
+					"Special scan error in authorization form value."));
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
+std::vector<Scope> ComputeScopes(const Form &form) {
+	auto result = std::vector<Scope>();
 	const auto findValue = [&](const Value::Type type) {
 		const auto i = form.values.find(type);
 		Assert(i != form.values.end());
 		return &i->second;
 	};
-	for (const auto type : form.request) {
-		const auto scopeType = ScopeTypeForValueType(type);
-		const auto fieldsType = FieldsTypeForScopeType(scopeType);
-		const auto [i, ok] = scopes.emplace(
-			scopeType,
-			Scope(scopeType, findValue(fieldsType)));
-		i->second.selfieRequired = (scopeType == Scope::Type::Identity)
-			&& form.identitySelfieRequired;
-		const auto alreadyIt = ranges::find(
-			i->second.documents,
-			type,
-			[](not_null<const Value*> value) { return value->type; });
-		if (alreadyIt != end(i->second.documents)) {
-			LOG(("API Error: Value type %1 multiple times in request."
-				).arg(int(type)));
+	for (const auto &requested : form.request) {
+		Assert(!requested.empty());
+		const auto scopeType = ScopeTypeForValueType(requested[0]);
+		const auto detailsType = DetailsTypeForScopeType(scopeType);
+		const auto ownsDetails = (scopeType != Scope::Type::Identity
+			&& scopeType != Scope::Type::Address);
+		const auto inlineDetails = InlineDetails(form.request, detailsType);
+		if (ownsDetails && inlineDetails) {
 			continue;
-		} else if (type != fieldsType) {
-			i->second.documents.push_back(findValue(type));
 		}
-	}
-	auto result = std::vector<Scope>();
-	result.reserve(scopes.size());
-	for (auto &[type, scope] : scopes) {
-		result.push_back(std::move(scope));
+		result.push_back(Scope(scopeType));
+		auto &scope = result.back();
+		scope.details = (ownsDetails || inlineDetails)
+			? findValue(detailsType)
+			: nullptr;
+		if (ownsDetails) {
+			Assert(requested.size() == 1);
+		} else {
+			for (const auto type : requested) {
+				scope.documents.push_back(findValue(type));
+			}
+		}
 	}
 	return result;
 }
@@ -129,7 +208,9 @@ QString JoinScopeRowReadyString(
 
 QString ComputeScopeRowReadyString(const Scope &scope) {
 	switch (scope.type) {
+	case Scope::Type::PersonalDetails:
 	case Scope::Type::Identity:
+	case Scope::Type::AddressDetails:
 	case Scope::Type::Address: {
 		auto list = std::vector<std::pair<QString, QString>>();
 		const auto pushListValue = [&](
@@ -153,10 +234,12 @@ QString ComputeScopeRowReadyString(const Scope &scope) {
 				}
 			}
 		};
-		const auto &fields = scope.fields->data.parsed.fields;
+		const auto fields = scope.details
+			? &scope.details->data.parsed.fields
+			: nullptr;
 		const auto document = [&]() -> const Value* {
 			for (const auto &document : scope.documents) {
-				if (document->scansAreFilled(scope.selfieRequired)) {
+				if (document->scansAreFilled()) {
 					return document;
 				}
 			}
@@ -195,8 +278,11 @@ QString ComputeScopeRowReadyString(const Scope &scope) {
 		for (const auto &row : scheme.rows) {
 			const auto format = row.format;
 			if (row.valueClass == EditDocumentScheme::ValueClass::Fields) {
-				const auto i = fields.find(row.key);
-				if (i == end(fields)) {
+				if (!fields) {
+					continue;
+				}
+				const auto i = fields->find(row.key);
+				if (i == end(*fields)) {
 					return QString();
 				}
 				const auto text = i->second.text;
@@ -225,8 +311,9 @@ QString ComputeScopeRowReadyString(const Scope &scope) {
 	} break;
 	case Scope::Type::Phone:
 	case Scope::Type::Email: {
+		Assert(scope.details != nullptr);
 		const auto format = GetContactScheme(scope.type).format;
-		const auto &fields = scope.fields->data.parsed.fields;
+		const auto &fields = scope.details->data.parsed.fields;
 		const auto i = fields.find("value");
 		return (i != end(fields))
 			? (format ? format(i->second.text) : i->second.text)
@@ -242,19 +329,30 @@ ScopeRow ComputeScopeRow(const Scope &scope) {
 		row.ready = ready;
 		auto errors = QStringList();
 		const auto addValueErrors = [&](not_null<const Value*> value) {
+			if (!value->error.isEmpty()) {
+				errors.push_back(value->error);
+			}
+			if (!value->scanMissingError.isEmpty()) {
+				errors.push_back(value->scanMissingError);
+			}
+			if (!value->translationMissingError.isEmpty()) {
+				errors.push_back(value->translationMissingError);
+			}
 			for (const auto &scan : value->scans) {
 				if (!scan.error.isEmpty()) {
 					errors.push_back(scan.error);
 				}
 			}
+			for (const auto &scan : value->translations) {
+				if (!scan.error.isEmpty()) {
+					errors.push_back(scan.error);
+				}
+			}
 			for (const auto &[type, scan] : value->specialScans) {
 				if (!scan.error.isEmpty()) {
 					errors.push_back(scan.error);
 				}
 			}
-			if (!value->scanMissingError.isEmpty()) {
-				errors.push_back(value->scanMissingError);
-			}
 			for (const auto &[key, value] : value->data.parsed.fields) {
 				if (!value.error.isEmpty()) {
 					errors.push_back(value.error);
@@ -263,7 +361,7 @@ ScopeRow ComputeScopeRow(const Scope &scope) {
 		};
 		const auto document = [&]() -> const Value* {
 			for (const auto &document : scope.documents) {
-				if (document->scansAreFilled(scope.selfieRequired)) {
+				if (document->scansAreFilled()) {
 					return document;
 				}
 			}
@@ -272,31 +370,35 @@ ScopeRow ComputeScopeRow(const Scope &scope) {
 		if (document) {
 			addValueErrors(document);
 		}
-		addValueErrors(scope.fields);
+		if (scope.details) {
+			addValueErrors(scope.details);
+		}
 		if (!errors.isEmpty()) {
-			row.error = lang(lng_passport_fix_errors);// errors.join('\n');
-		}
-		if (row.error.isEmpty()
-			&& row.ready.isEmpty()
-			&& scope.type == Scope::Type::Identity
-			&& scope.selfieRequired) {
-			auto noSelfieScope = scope;
-			noSelfieScope.selfieRequired = false;
-			if (!ComputeScopeRowReadyString(noSelfieScope).isEmpty()) {
-				// Only selfie is missing.
-				row.description = lang(lng_passport_identity_selfie);
-			}
+			row.error = errors[0];// errors.join('\n');
 		}
+		// #TODO passport half-full value
+		//if (row.error.isEmpty()
+		//	&& row.ready.isEmpty()
+		//	&& scope.type == Scope::Type::Identity
+		//	&& scope.selfieRequired) {
+		//	auto noSelfieScope = scope;
+		//	noSelfieScope.selfieRequired = false;
+		//	if (!ComputeScopeRowReadyString(noSelfieScope).isEmpty()) {
+		//		// Only selfie is missing.
+		//		row.description = lang(lng_passport_identity_selfie);
+		//	}
+		//}
 		return row;
 	};
 	switch (scope.type) {
+	case Scope::Type::PersonalDetails:
+		return addReadyError({
+			lang(lng_passport_personal_details),
+			lang(lng_passport_personal_details_enter),
+		});
 	case Scope::Type::Identity:
-		if (scope.documents.empty()) {
-			return addReadyError({
-				lang(lng_passport_personal_details),
-				lang(lng_passport_personal_details_enter),
-			});
-		} else if (scope.documents.size() == 1) {
+		Assert(!scope.documents.empty());
+		if (scope.documents.size() == 1) {
 			switch (scope.documents.front()->type) {
 			case Value::Type::Passport:
 				return addReadyError({
@@ -325,13 +427,14 @@ ScopeRow ComputeScopeRow(const Scope &scope) {
 			lang(lng_passport_identity_title),
 			lang(lng_passport_identity_description),
 		});
-	case Scope::Type::Address:
-		if (scope.documents.empty()) {
-			return addReadyError({
-				lang(lng_passport_address),
-				lang(lng_passport_address_enter),
+	case Scope::Type::AddressDetails:
+		return addReadyError({
+			lang(lng_passport_address),
+			lang(lng_passport_address_enter),
 			});
-		} else if (scope.documents.size() == 1) {
+	case Scope::Type::Address:
+		Assert(!scope.documents.empty());
+		if (scope.documents.size() == 1) {
 			switch (scope.documents.front()->type) {
 			case Value::Type::BankStatement:
 				return addReadyError({
diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h
index b32729345..925c15a41 100644
--- a/Telegram/SourceFiles/passport/passport_form_view_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h
@@ -13,17 +13,18 @@ namespace Passport {
 
 struct Scope {
 	enum class Type {
+		PersonalDetails,
 		Identity,
+		AddressDetails,
 		Address,
 		Phone,
 		Email,
 	};
-	Scope(Type type, not_null<const Value*> fields);
+	explicit Scope(Type type);
 
 	Type type;
-	not_null<const Value*> fields;
+	const Value *details = nullptr;
 	std::vector<not_null<const Value*>> documents;
-	bool selfieRequired = false;
 };
 
 struct ScopeRow {
@@ -33,8 +34,8 @@ struct ScopeRow {
 	QString error;
 };
 
-std::vector<Scope> ComputeScopes(
-	not_null<const FormController*> controller);
+bool ValidateForm(const Form &form);
+std::vector<Scope> ComputeScopes(const Form &form);
 QString ComputeScopeRowReadyString(const Scope &scope);
 ScopeRow ComputeScopeRow(const Scope &scope);
 
diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
index c7519e2be..5895895cf 100644
--- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
@@ -99,6 +99,7 @@ EditDocumentScheme GetDocumentScheme(
 		return !CountryFormat(value).isEmpty();
 	});
 
+	// #TODO passport scheme
 	switch (type) {
 	case Scope::Type::Identity: {
 		auto result = Scheme();
@@ -360,7 +361,7 @@ BoxContent *BoxPointer::operator->() const {
 
 PanelController::PanelController(not_null<FormController*> form)
 : _form(form)
-, _scopes(ComputeScopes(_form)) {
+, _scopes(ComputeScopes(_form->form())) {
 	_form->secretReadyEvents(
 	) | rpl::start_with_next([=] {
 		ensurePanelCreated();
@@ -378,8 +379,6 @@ PanelController::PanelController(not_null<FormController*> form)
 	}) | rpl::start_with_next([=](not_null<const Value*> field) {
 		_verificationBoxes.erase(field);
 	}, lifetime());
-
-	_scopes = ComputeScopes(_form);
 }
 
 not_null<UserData*> PanelController::bot() const {
@@ -397,12 +396,14 @@ void PanelController::fillRows(
 		bool ready,
 		bool error)> callback) {
 	if (_scopes.empty()) {
-		_scopes = ComputeScopes(_form);
+		_scopes = ComputeScopes(_form->form());
 	}
 	for (const auto &scope : _scopes) {
 		const auto row = ComputeScopeRow(scope);
-		const auto main = scope.fields;
-		if (!row.ready.isEmpty()) {
+		const auto main = scope.details
+			? not_null<const Value*>(scope.details)
+			: scope.documents[0];
+		if (main && !row.ready.isEmpty()) {
 			_submitErrors.erase(
 				ranges::remove(_submitErrors, main),
 				_submitErrors.end());
@@ -546,9 +547,7 @@ void PanelController::uploadSpecialScan(
 		QByteArray &&content) {
 	Expects(_editScope != nullptr);
 	Expects(_editDocument != nullptr);
-	Expects(_editDocument->requiresSpecialScan(
-		type,
-		_editScope->selfieRequired));
+	Expects(_editDocument->requiresSpecialScan(type));
 
 	_form->uploadSpecialScan(_editDocument, type, std::move(content));
 }
@@ -556,9 +555,7 @@ void PanelController::uploadSpecialScan(
 void PanelController::deleteSpecialScan(SpecialFile type) {
 	Expects(_editScope != nullptr);
 	Expects(_editDocument != nullptr);
-	Expects(_editDocument->requiresSpecialScan(
-		type,
-		_editScope->selfieRequired));
+	Expects(_editDocument->requiresSpecialScan(type));
 
 	_form->deleteSpecialScan(_editDocument, type);
 }
@@ -566,9 +563,7 @@ void PanelController::deleteSpecialScan(SpecialFile type) {
 void PanelController::restoreSpecialScan(SpecialFile type) {
 	Expects(_editScope != nullptr);
 	Expects(_editDocument != nullptr);
-	Expects(_editDocument->requiresSpecialScan(
-		type,
-		_editScope->selfieRequired));
+	Expects(_editDocument->requiresSpecialScan(type));
 
 	_form->restoreSpecialScan(_editDocument, type);
 }
@@ -642,9 +637,15 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
 std::vector<ScopeError> PanelController::collectErrors(
 		not_null<const Value*> value) const {
 	auto result = std::vector<ScopeError>();
+	if (!value->error.isEmpty()) {
+		result.push_back({ FileKey(), value->error });
+	}
 	if (!value->scanMissingError.isEmpty()) {
 		result.push_back({ FileKey(), value->scanMissingError });
 	}
+	if (!value->translationMissingError.isEmpty()) {
+		result.push_back({ FileKey(), value->translationMissingError });
+	}
 	const auto addFileError = [&](const EditFile &file) {
 		if (!file.fields.error.isEmpty()) {
 			const auto key = FileKey{ file.fields.id, file.fields.dcId };
@@ -654,6 +655,9 @@ std::vector<ScopeError> PanelController::collectErrors(
 	for (const auto &scan : value->scansInEdit) {
 		addFileError(scan);
 	}
+	for (const auto &scan : value->translationsInEdit) {
+		addFileError(scan);
+	}
 	for (const auto &[type, scan] : value->specialScansInEdit) {
 		addFileError(scan);
 	}
@@ -671,13 +675,14 @@ auto PanelController::deleteValueLabel() const
 
 	if (hasValueDocument()) {
 		return Lang::Viewer(lng_passport_delete_document);
-	}
-	if (!hasValueFields()) {
+	} else if (!hasValueFields()) {
 		return base::none;
 	}
 	switch (_editScope->type) {
+	case Scope::Type::PersonalDetails:
 	case Scope::Type::Identity:
 		return Lang::Viewer(lng_passport_delete_details);
+	case Scope::Type::AddressDetails:
 	case Scope::Type::Address:
 		return Lang::Viewer(lng_passport_delete_address);
 	case Scope::Type::Email:
@@ -700,27 +705,26 @@ bool PanelController::hasValueDocument() const {
 }
 
 bool PanelController::hasValueFields() const {
-	Expects(_editValue != nullptr);
-
-	return !_editValue->data.parsed.fields.empty();
+	return _editValue && !_editValue->data.parsed.fields.empty();
 }
 
 void PanelController::deleteValue() {
 	Expects(_editScope != nullptr);
+	Expects(hasValueDocument() || hasValueFields());
 
 	if (savingScope()) {
 		return;
 	}
 	const auto text = [&] {
 		switch (_editScope->type) {
+		case Scope::Type::PersonalDetails:
+			return lang(lng_passport_delete_details_sure);
 		case Scope::Type::Identity:
-			return lang(hasValueDocument()
-				? lng_passport_delete_document_sure
-				: lng_passport_delete_details_sure);
+			return lang(lng_passport_delete_document_sure);
+		case Scope::Type::AddressDetails:
+			return lang(lng_passport_delete_address_sure);
 		case Scope::Type::Address:
-			return lang(hasValueDocument()
-				? lng_passport_delete_document_sure
-				: lng_passport_delete_address_sure);
+			return lang(lng_passport_delete_document_sure);
 		case Scope::Type::Phone:
 			return lang(lng_passport_delete_phone_sure);
 		case Scope::Type::Email:
@@ -745,7 +749,7 @@ void PanelController::deleteValue() {
 }
 
 void PanelController::deleteValueSure(bool withDetails) {
-	Expects(_editValue != nullptr);
+	Expects(!withDetails || _editValue != nullptr);
 
 	if (hasValueDocument()) {
 		_form->deleteValueEdit(_editDocument);
@@ -830,21 +834,22 @@ int PanelController::findNonEmptyDocumentIndex(const Scope &scope) const {
 	const auto &documents = scope.documents;
 	const auto i = ranges::find_if(
 		documents,
-		[&](not_null<const Value*> document) {
-			return document->scansAreFilled(scope.selfieRequired);
+		[](not_null<const Value*> document) {
+			return document->scansAreFilled();
 		});
 	if (i != end(documents)) {
 		return (i - begin(documents));
 	}
 	// If we have a document where only selfie is not filled - return it.
-	const auto j = ranges::find_if(
-		documents,
-		[&](not_null<const Value*> document) {
-			return document->scansAreFilled(false);
-		});
-	if (j != end(documents)) {
-		return (j - begin(documents));
-	}
+	// #TODO passport half-full value
+	//const auto j = ranges::find_if(
+	//	documents,
+	//	[&](not_null<const Value*> document) {
+	//		return document->scansAreFilled(false);
+	//	});
+	//if (j != end(documents)) {
+	//	return (j - begin(documents));
+	//}
 	return -1;
 }
 
@@ -936,8 +941,7 @@ void PanelController::editWithUpload(int index, int documentIndex) {
 
 	const auto &document = _scopes[index].documents[documentIndex];
 	const auto requiresSpecialScan = document->requiresSpecialScan(
-		SpecialFile::FrontSide,
-		false);
+		SpecialFile::FrontSide);
 	const auto allowMany = !requiresSpecialScan;
 	const auto widget = _panel->widget();
 	EditScans::ChooseScan(widget.get(), [=](QByteArray &&content) {
@@ -981,21 +985,26 @@ void PanelController::editScope(int index, int documentIndex) {
 			&& documentIndex < _scopes[index].documents.size()));
 
 	_editScope = &_scopes[index];
-	_editValue = _editScope->fields;
+	_editValue = _editScope->details;
 	_editDocument = (documentIndex >= 0)
 		? _scopes[index].documents[documentIndex].get()
 		: nullptr;
+	Assert(_editValue || _editDocument);
 
-	_form->startValueEdit(_editValue);
+	if (_editValue) {
+		_form->startValueEdit(_editValue);
+	}
 	if (_editDocument) {
 		_form->startValueEdit(_editDocument);
 	}
 
 	auto content = [&]() -> object_ptr<Ui::RpWidget> {
+		// #TODO passport pass and display value->error
 		switch (_editScope->type) {
 		case Scope::Type::Identity:
 		case Scope::Type::Address: {
-			auto result = _editDocument
+			Assert(_editDocument != nullptr);
+			auto result = _editValue
 				? object_ptr<PanelEditDocument>(
 					_panel->widget(),
 					this,
@@ -1007,6 +1016,7 @@ void PanelController::editScope(int index, int documentIndex) {
 					_editDocument->scanMissingError,
 					valueFiles(*_editDocument),
 					valueSpecialFiles(*_editDocument))
+				// #TODO passport document without details
 				: object_ptr<PanelEditDocument>(
 					_panel->widget(),
 					this,
@@ -1018,8 +1028,23 @@ void PanelController::editScope(int index, int documentIndex) {
 			};
 			return std::move(result);
 		} break;
+		case Scope::Type::PersonalDetails:
+		case Scope::Type::AddressDetails: {
+			Assert(_editValue != nullptr);
+			auto result = object_ptr<PanelEditDocument>(
+					_panel->widget(),
+					this,
+					GetDocumentScheme(_editScope->type),
+					_editValue->data.parsedInEdit);
+			const auto weak = make_weak(result.data());
+			_panelHasUnsavedChanges = [=] {
+				return weak ? weak->hasUnsavedChanges() : false;
+			};
+			return std::move(result);
+		} break;
 		case Scope::Type::Phone:
 		case Scope::Type::Email: {
+			Assert(_editValue != nullptr);
 			const auto &parsed = _editValue->data.parsedInEdit;
 			const auto valueIt = parsed.fields.find("value");
 			const auto value = (valueIt == end(parsed.fields)
@@ -1081,16 +1106,12 @@ void PanelController::processValueSaveFinished(
 }
 
 bool PanelController::uploadingScopeScan() const {
-	Expects(_editValue != nullptr);
-
-	return _form->uploadingScan(_editValue)
+	return (_editValue && _form->uploadingScan(_editValue))
 		|| (_editDocument && _form->uploadingScan(_editDocument));
 }
 
 bool PanelController::savingScope() const {
-	Expects(_editValue != nullptr);
-
-	return _form->savingValue(_editValue)
+	return (_editValue && _form->savingValue(_editValue))
 		|| (_editDocument && _form->savingValue(_editDocument));
 }
 
@@ -1173,7 +1194,7 @@ std::map<SpecialFile, ScanInfo> PanelController::valueSpecialFiles(
 		SpecialFile::Selfie
 	};
 	for (const auto type : types) {
-		if (value.requiresSpecialScan(type, _editScope->selfieRequired)) {
+		if (value.requiresSpecialScan(type)) {
 			const auto i = value.specialScansInEdit.find(type);
 			const auto j = result.emplace(
 				type,
@@ -1190,7 +1211,9 @@ void PanelController::cancelValueEdit() {
 	Expects(_editScope != nullptr);
 
 	_editScopeBoxes.clear();
-	_form->cancelValueEdit(base::take(_editValue));
+	if (const auto value = base::take(_editValue)) {
+		_form->cancelValueEdit(value);
+	}
 	if (const auto document = base::take(_editDocument)) {
 		_form->cancelValueEdit(document);
 	}
@@ -1199,7 +1222,6 @@ void PanelController::cancelValueEdit() {
 
 void PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) {
 	Expects(_panel != nullptr);
-	Expects(_editValue != nullptr);
 
 	if (uploadingScopeScan()) {
 		showToast(lang(lng_passport_wait_upload));
@@ -1208,7 +1230,9 @@ void PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) {
 		return;
 	}
 
-	_form->saveValueEdit(_editValue, std::move(data));
+	if (_editValue) {
+		_form->saveValueEdit(_editValue, std::move(data));
+	}
 	if (_editDocument) {
 		_form->saveValueEdit(_editDocument, std::move(filesData));
 	} else {
@@ -1219,12 +1243,11 @@ void PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) {
 bool PanelController::editScopeChanged(
 		const ValueMap &data,
 		const ValueMap &filesData) const {
-	Expects(_editValue != nullptr);
-
-	if (_form->editValueChanged(_editValue, data)) {
+	if (_editValue && _form->editValueChanged(_editValue, data)) {
+		return true;
+	} else if (_editDocument
+		&& _form->editValueChanged(_editDocument, filesData)) {
 		return true;
-	} else if (_editDocument) {
-		return _form->editValueChanged(_editDocument, filesData);
 	}
 	return false;
 }