diff --git a/Telegram/SourceFiles/base/qthelp_url.cpp b/Telegram/SourceFiles/base/qthelp_url.cpp
index 5ced7c84e..820ee5de3 100644
--- a/Telegram/SourceFiles/base/qthelp_url.cpp
+++ b/Telegram/SourceFiles/base/qthelp_url.cpp
@@ -44,4 +44,16 @@ bool is_ipv6(const QString &ip) {
 	return ip.indexOf('.') < 0 && ip.indexOf(':') >= 0;
 }
 
+QString url_append_query(const QString &url, const QString &add) {
+	const auto query = ranges::find(url, '?');
+	const auto hash = ranges::find(url, '#');
+	const auto base = url.mid(0, hash - url.begin());
+	const auto added = base
+		+ (hash <= query ? '?' : '&')
+		+ add;
+	const auto result = added
+		+ (hash < url.end() ? url.mid(hash - url.begin()) : QString());
+	return result;
+}
+
 } // namespace qthelp
diff --git a/Telegram/SourceFiles/base/qthelp_url.h b/Telegram/SourceFiles/base/qthelp_url.h
index 81b31f67e..4a32406c6 100644
--- a/Telegram/SourceFiles/base/qthelp_url.h
+++ b/Telegram/SourceFiles/base/qthelp_url.h
@@ -22,7 +22,11 @@ enum class UrlParamNameTransform {
 	ToLower,
 };
 // Parses a string like "p1=v1&p2=v2&..&pn=vn" to a map.
-QMap<QString, QString> url_parse_params(const QString &params, UrlParamNameTransform transform = UrlParamNameTransform::NoTransform);
+QMap<QString, QString> url_parse_params(
+	const QString &params,
+	UrlParamNameTransform transform = UrlParamNameTransform::NoTransform);
+
+QString url_append_query(const QString &url, const QString &add);
 
 bool is_ipv6(const QString &ip);
 
diff --git a/Telegram/SourceFiles/messenger.cpp b/Telegram/SourceFiles/messenger.cpp
index 441b29314..f71d179dc 100644
--- a/Telegram/SourceFiles/messenger.cpp
+++ b/Telegram/SourceFiles/messenger.cpp
@@ -841,13 +841,15 @@ bool Messenger::openLocalUrl(const QString &url) {
 			const auto scope = params.value("scope", QString());
 			const auto callback = params.value("callback_url", QString());
 			const auto publicKey = params.value("public_key", QString());
+			const auto payload = params.value("payload", QString());
 			if (const auto window = App::wnd()) {
 				if (const auto controller = window->controller()) {
 					controller->showPassportForm(Passport::FormRequest(
 						botId,
 						scope,
 						callback,
-						publicKey));
+						publicKey,
+						payload));
 					return true;
 				}
 			}
diff --git a/Telegram/SourceFiles/mtproto/rsa_public_key.cpp b/Telegram/SourceFiles/mtproto/rsa_public_key.cpp
index 2ead24f7e..5d96b8131 100644
--- a/Telegram/SourceFiles/mtproto/rsa_public_key.cpp
+++ b/Telegram/SourceFiles/mtproto/rsa_public_key.cpp
@@ -164,6 +164,29 @@ public:
 		}
 		return result;
 	}
+	bytes::vector encryptOAEPpadding(bytes::const_span data) const {
+		Expects(isValid());
+
+		const auto resultSize = RSA_size(_rsa);
+		auto result = bytes::vector(resultSize, gsl::byte{});
+		const auto encryptedSize = RSA_public_encrypt(
+			data.size(),
+			reinterpret_cast<const unsigned char*>(data.data()),
+			reinterpret_cast<unsigned char*>(result.data()),
+			_rsa,
+			RSA_PKCS1_OAEP_PADDING);
+		if (encryptedSize != resultSize) {
+			ERR_load_crypto_strings();
+			LOG(("RSA Error: RSA_public_encrypt failed, "
+				"key fp: %1, result: %2, error: %3"
+				).arg(getFingerPrint()
+				).arg(encryptedSize
+				).arg(ERR_error_string(ERR_get_error(), 0)
+				));
+			return {};
+		}
+		return result;
+	}
 	~Private() {
 		RSA_free(_rsa);
 	}
@@ -236,5 +259,10 @@ bytes::vector RSAPublicKey::decrypt(bytes::const_span data) const {
 	return _private->decrypt(data);
 }
 
+bytes::vector RSAPublicKey::encryptOAEPpadding(
+		bytes::const_span data) const {
+	return _private->encryptOAEPpadding(data);
+}
+
 } // namespace internal
 } // namespace MTP
diff --git a/Telegram/SourceFiles/mtproto/rsa_public_key.h b/Telegram/SourceFiles/mtproto/rsa_public_key.h
index 7c03914b8..8e748d8a5 100644
--- a/Telegram/SourceFiles/mtproto/rsa_public_key.h
+++ b/Telegram/SourceFiles/mtproto/rsa_public_key.h
@@ -37,6 +37,9 @@ public:
 	// data has exactly 256 chars to be decrypted
 	bytes::vector decrypt(bytes::const_span data) const;
 
+	// data has lequal than 215 chars to be decrypted
+	bytes::vector encryptOAEPpadding(bytes::const_span data) const;
+
 private:
 	class Private;
 	std::shared_ptr<Private> _private;
diff --git a/Telegram/SourceFiles/passport/passport_encryption.cpp b/Telegram/SourceFiles/passport/passport_encryption.cpp
index 462e4d326..efe5ed5bb 100644
--- a/Telegram/SourceFiles/passport/passport_encryption.cpp
+++ b/Telegram/SourceFiles/passport/passport_encryption.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "passport/passport_encryption.h"
 
 #include "base/openssl_help.h"
+#include "mtproto/rsa_public_key.h"
 
 namespace Passport {
 namespace {
@@ -314,14 +315,6 @@ bytes::vector PrepareValueHash(
 	return openssl::Sha256(bytes::concatenate(dataHash, valueSecret));
 }
 
-bytes::vector PrepareFilesHash(
-		gsl::span<bytes::const_span> fileHashes,
-		bytes::const_span valueSecret) {
-	return openssl::Sha256(bytes::concatenate(
-		bytes::concatenate(fileHashes),
-		valueSecret));
-}
-
 bytes::vector EncryptValueSecret(
 		bytes::const_span valueSecret,
 		bytes::const_span secret,
@@ -350,4 +343,11 @@ uint64 CountSecureSecretHash(bytes::const_span secret) {
 	return *reinterpret_cast<const uint64*>(part.data());
 }
 
+bytes::vector EncryptCredentialsSecret(
+		bytes::const_span secret,
+		bytes::const_span publicKey) {
+	const auto key = MTP::internal::RSAPublicKey(publicKey);
+	return key.encryptOAEPpadding(secret);
+}
+
 } // namespace Passport
diff --git a/Telegram/SourceFiles/passport/passport_encryption.h b/Telegram/SourceFiles/passport/passport_encryption.h
index 6f2ff2e0c..d03041e7b 100644
--- a/Telegram/SourceFiles/passport/passport_encryption.h
+++ b/Telegram/SourceFiles/passport/passport_encryption.h
@@ -54,10 +54,10 @@ bytes::vector DecryptValueSecret(
 	bytes::const_span secret,
 	bytes::const_span valueHash);
 
-bytes::vector PrepareFilesHash(
-	gsl::span<bytes::const_span> fileHashes,
-	bytes::const_span valueSecret);
-
 uint64 CountSecureSecretHash(bytes::const_span secret);
 
+bytes::vector EncryptCredentialsSecret(
+	bytes::const_span secret,
+	bytes::const_span publicKey);
+
 } // namespace Passport
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp
index bde85a5c5..f10047fb1 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp
@@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "boxes/confirm_box.h"
 #include "lang/lang_keys.h"
 #include "base/openssl_help.h"
+#include "base/qthelp_url.h"
 #include "mainwindow.h"
 #include "window/window_controller.h"
+#include "core/click_handler_types.h"
 #include "auth_session.h"
 #include "storage/localimageloader.h"
 #include "storage/localstorage.h"
@@ -46,17 +48,86 @@ Value::Type ConvertType(const MTPSecureValueType &type) {
 	Unexpected("Type in secureValueType type.");
 };
 
+MTPSecureValueType ConvertType(Value::Type type) {
+	switch (type) {
+	case Value::Type::PersonalDetails:
+		return MTP_secureValueTypePersonalDetails();
+	case Value::Type::Passport:
+		return MTP_secureValueTypePassport();
+	case Value::Type::DriverLicense:
+		return MTP_secureValueTypeDriverLicense();
+	case Value::Type::IdentityCard:
+		return MTP_secureValueTypeIdentityCard();
+	case Value::Type::Address:
+		return MTP_secureValueTypeAddress();
+	case Value::Type::UtilityBill:
+		return MTP_secureValueTypeUtilityBill();
+	case Value::Type::BankStatement:
+		return MTP_secureValueTypeBankStatement();
+	case Value::Type::RentalAgreement:
+		return MTP_secureValueTypeRentalAgreement();
+	case Value::Type::Phone:
+		return MTP_secureValueTypePhone();
+	case Value::Type::Email:
+		return MTP_secureValueTypeEmail();
+	}
+	Unexpected("Type in FormController::submit.");
+};
+
+QJsonObject GetJSONFromMap(
+		const std::map<QString, bytes::const_span> &map) {
+	auto result = QJsonObject();
+	for (const auto &[key, value] : map) {
+		const auto raw = QByteArray::fromRawData(
+			reinterpret_cast<const char*>(value.data()),
+			value.size());
+		result.insert(key, QString::fromUtf8(raw.toBase64()));
+	}
+	return result;
+}
+
+QJsonObject GetJSONFromFile(const File &file) {
+	return GetJSONFromMap({
+		{ "file_hash", file.hash },
+		{ "secret", file.secret }
+	});
+}
+
+FormRequest PreprocessRequest(const FormRequest &request) {
+	auto result = request;
+	result.publicKey.replace("\r\n", "\n");
+	return result;
+}
+
+QString ValueCredentialsKey(Value::Type type) {
+	switch (type) {
+	case Value::Type::PersonalDetails: return "personal_details";
+	case Value::Type::Passport: return "passport";
+	case Value::Type::DriverLicense: return "driver_license";
+	case Value::Type::IdentityCard: return "identity_card";
+	case Value::Type::Address: return "address";
+	case Value::Type::UtilityBill: return "utility_bill";
+	case Value::Type::BankStatement: return "bank_statement";
+	case Value::Type::RentalAgreement: return "rental_agreement";
+	case Value::Type::Phone:
+	case Value::Type::Email: return QString();
+	}
+	Unexpected("Type in ValueCredentialsKey.");
+}
+
 } // namespace
 
 FormRequest::FormRequest(
 	UserId botId,
 	const QString &scope,
 	const QString &callbackUrl,
-	const QString &publicKey)
+	const QString &publicKey,
+	const QString &payload)
 : botId(botId)
 , scope(scope)
 , callbackUrl(callbackUrl)
-, publicKey(publicKey) {
+, publicKey(publicKey)
+, payload(payload) {
 }
 
 EditFile::EditFile(
@@ -111,7 +182,7 @@ FormController::FormController(
 	not_null<Window::Controller*> controller,
 	const FormRequest &request)
 : _controller(controller)
-, _request(request)
+, _request(PreprocessRequest(request))
 , _view(std::make_unique<PanelController>(this)) {
 }
 
@@ -136,6 +207,99 @@ bytes::vector FormController::passwordHashForAuth(
 		_password.salt));
 }
 
+auto FormController::prepareFinalData() const -> FinalData {
+	auto hashes = QVector<MTPSecureValueHash>();
+	auto secureData = QJsonObject();
+	const auto addValueToJSON = [&](
+			const QString &key,
+			not_null<const Value*> value) {
+		auto object = QJsonObject();
+		if (!value->data.parsed.fields.empty()) {
+			object.insert("data", GetJSONFromMap({
+				{ "data_hash", value->data.hash },
+				{ "secret", value->data.secret }
+			}));
+		}
+		if (!value->scans.empty()) {
+			auto files = QJsonArray();
+			for (const auto &scan : value->scans) {
+				files.append(GetJSONFromFile(scan));
+			}
+			object.insert("files", files);
+		}
+		if (_form.identitySelfieRequired && value->selfie) {
+			object.insert("selfie", GetJSONFromFile(*value->selfie));
+		}
+		secureData.insert(key, object);
+	};
+	const auto addValue = [&](not_null<const Value*> value) {
+		hashes.push_back(MTP_secureValueHash(
+			ConvertType(value->type),
+			MTP_bytes(value->submitHash)));
+		const auto key = ValueCredentialsKey(value->type);
+		if (!key.isEmpty()) {
+			addValueToJSON(key, value);
+		}
+	};
+	const auto scopes = ComputeScopes(this);
+	for (const auto &scope : scopes) {
+		const auto ready = ComputeScopeRowReadyString(scope);
+		if (ready.isEmpty()) {
+			_valueError.fire_copy(scope.fields);
+		}
+		addValue(scope.fields);
+		if (!scope.documents.empty()) {
+			for (const auto &document : scope.documents) {
+				if (!document->scans.empty()) {
+					addValue(document);
+					break;
+				}
+			}
+		}
+	}
+
+	auto json = QJsonObject();
+	json.insert("secure_data", secureData);
+	json.insert("payload", _request.payload);
+
+	return {
+		hashes,
+		QJsonDocument(json).toJson(QJsonDocument::Compact)
+	};
+}
+
+void FormController::submit() {
+	if (_submitRequestId) {
+		return;
+	}
+
+	const auto prepared = prepareFinalData();
+	const auto credentialsEncryptedData = EncryptData(
+		bytes::make_span(prepared.credentials));
+	const auto credentialsEncryptedSecret = EncryptCredentialsSecret(
+		credentialsEncryptedData.secret,
+		bytes::make_span(_request.publicKey.toUtf8()));
+
+	_submitRequestId = request(MTPaccount_AcceptAuthorization(
+		MTP_int(_request.botId),
+		MTP_string(_request.scope),
+		MTP_string(_request.publicKey),
+		MTP_vector<MTPSecureValueHash>(prepared.hashes),
+		MTP_secureCredentialsEncrypted(
+			MTP_bytes(credentialsEncryptedData.bytes),
+			MTP_bytes(credentialsEncryptedData.hash),
+			MTP_bytes(credentialsEncryptedSecret))
+	)).done([=](const MTPBool &result) {
+		const auto url = qthelp::url_append_query(
+			_request.callbackUrl,
+			"tg_passport=success");
+		UrlClickHandler::doOpen(url);
+	}).fail([=](const RPCError &error) {
+		_view->show(Box<InformBox>(
+			"Failed sending data :(\n" + error.type()));
+	}).send();
+}
+
 void FormController::submitPassword(const QString &password) {
 	Expects(!_password.salt.empty());
 
@@ -231,19 +395,7 @@ bool FormController::validateValueSecrets(Value &value) {
 			return false;
 		}
 	}
-	for (auto &file : value.files) {
-		file.secret = DecryptValueSecret(
-			file.encryptedSecret,
-			_secret,
-			file.hash);
-		if (file.secret.empty()) {
-			LOG(("API Error: Could not decrypt file secret. "
-				"Forgetting files and data :("));
-			return false;
-		}
-	}
-	if (value.selfie) {
-		auto &file = *value.selfie;
+	const auto validateFileSecret = [&](File &file) {
 		file.secret = DecryptValueSecret(
 			file.encryptedSecret,
 			_secret,
@@ -253,6 +405,15 @@ bool FormController::validateValueSecrets(Value &value) {
 				"Forgetting files and data :("));
 			return false;
 		}
+		return true;
+	};
+	for (auto &scan : value.scans) {
+		if (!validateFileSecret(scan)) {
+			return false;
+		}
+	}
+	if (value.selfie && !validateFileSecret(*value.selfie)) {
+		return false;
 	}
 	return true;
 }
@@ -273,31 +434,31 @@ void FormController::uploadScan(
 		not_null<const Value*> value,
 		QByteArray &&content) {
 	const auto nonconst = findValue(value);
-	auto fileIndex = int(nonconst->filesInEdit.size());
-	nonconst->filesInEdit.emplace_back(
+	auto scanIndex = int(nonconst->scansInEdit.size());
+	nonconst->scansInEdit.emplace_back(
 		nonconst,
 		File(),
 		nullptr);
-	auto &file = nonconst->filesInEdit.back();
-	encryptFile(file, std::move(content), [=](UploadScanData &&result) {
-		Expects(fileIndex >= 0 && fileIndex < nonconst->filesInEdit.size());
+	auto &scan = nonconst->scansInEdit.back();
+	encryptFile(scan, std::move(content), [=](UploadScanData &&result) {
+		Expects(scanIndex >= 0 && scanIndex < nonconst->scansInEdit.size());
 
 		uploadEncryptedFile(
-			nonconst->filesInEdit[fileIndex],
+			nonconst->scansInEdit[scanIndex],
 			std::move(result));
 	});
 }
 
 void FormController::deleteScan(
 		not_null<const Value*> value,
-		int fileIndex) {
-	scanDeleteRestore(value, fileIndex, true);
+		int scanIndex) {
+	scanDeleteRestore(value, scanIndex, true);
 }
 
 void FormController::restoreScan(
 		not_null<const Value*> value,
-		int fileIndex) {
-	scanDeleteRestore(value, fileIndex, false);
+		int scanIndex) {
+	scanDeleteRestore(value, scanIndex, false);
 }
 
 void FormController::uploadSelfie(
@@ -371,14 +532,14 @@ void FormController::encryptFile(
 
 void FormController::scanDeleteRestore(
 		not_null<const Value*> value,
-		int fileIndex,
+		int scanIndex,
 		bool deleted) {
-	Expects(fileIndex >= 0 && fileIndex < value->filesInEdit.size());
+	Expects(scanIndex >= 0 && scanIndex < value->scansInEdit.size());
 
 	const auto nonconst = findValue(value);
-	auto &file = nonconst->filesInEdit[fileIndex];
-	file.deleted = deleted;
-	_scanUpdated.fire(&file);
+	auto &scan = nonconst->scansInEdit[scanIndex];
+	scan.deleted = deleted;
+	_scanUpdated.fire(&scan);
 }
 
 void FormController::selfieDeleteRestore(
@@ -387,9 +548,9 @@ void FormController::selfieDeleteRestore(
 	Expects(value->selfieInEdit.has_value());
 
 	const auto nonconst = findValue(value);
-	auto &file = *nonconst->selfieInEdit;
-	file.deleted = deleted;
-	_scanUpdated.fire(&file);
+	auto &scan = *nonconst->selfieInEdit;
+	scan.deleted = deleted;
+	_scanUpdated.fire(&scan);
 }
 
 void FormController::subscribeToUploader() {
@@ -502,6 +663,11 @@ 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();
@@ -598,14 +764,14 @@ void FormController::startValueEdit(not_null<const Value*> value) {
 	if (savingValue(nonconst)) {
 		return;
 	}
-	for (auto &file : nonconst->files) {
-		loadFile(file);
+	for (auto &scan : nonconst->scans) {
+		loadFile(scan);
 	}
 	if (nonconst->selfie) {
 		loadFile(*nonconst->selfie);
 	}
-	nonconst->filesInEdit = ranges::view::all(
-		nonconst->files
+	nonconst->scansInEdit = ranges::view::all(
+		nonconst->scans
 	) | ranges::view::transform([=](const File &file) {
 		return EditFile(nonconst, file, nullptr);
 	}) | ranges::to_vector;
@@ -729,7 +895,7 @@ void FormController::clearValueEdit(not_null<Value*> value) {
 	if (savingValue(value)) {
 		return;
 	}
-	value->filesInEdit.clear();
+	value->scansInEdit.clear();
 	value->selfieInEdit = base::none;
 	value->data.encryptedSecretInEdit.clear();
 	value->data.hashInEdit.clear();
@@ -770,8 +936,8 @@ bool FormController::editValueChanged(
 		not_null<const Value*> value,
 		const ValueMap &data) const {
 	auto filesCount = 0;
-	for (const auto &file : value->filesInEdit) {
-		if (editFileChanged(file)) {
+	for (const auto &scan : value->scansInEdit) {
+		if (editFileChanged(scan)) {
 			return true;
 		}
 	}
@@ -804,7 +970,7 @@ void FormController::saveValueEdit(
 	if (!editValueChanged(nonconst, data)) {
 		nonconst->saveRequestId = -1;
 		crl::on_main(this, [=] {
-			base::take(nonconst->filesInEdit);
+			base::take(nonconst->scansInEdit);
 			base::take(nonconst->selfieInEdit);
 			base::take(nonconst->data.encryptedSecretInEdit);
 			base::take(nonconst->data.hashInEdit);
@@ -848,12 +1014,12 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 	};
 
 	auto inputFiles = QVector<MTPInputSecureFile>();
-	inputFiles.reserve(value->filesInEdit.size());
-	for (const auto &file : value->filesInEdit) {
-		if (file.deleted) {
+	inputFiles.reserve(value->scansInEdit.size());
+	for (const auto &scan : value->scansInEdit) {
+		if (scan.deleted) {
 			continue;
 		}
-		inputFiles.push_back(inputFile(file));
+		inputFiles.push_back(inputFile(scan));
 	}
 
 	if (value->data.secret.empty()) {
@@ -894,11 +1060,11 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
 		}
 		Unexpected("Value type in saveEncryptedValue().");
 	}();
-	const auto flags = ((value->filesInEdit.empty()
+	const auto flags = ((value->scansInEdit.empty()
 		&& value->data.parsedInEdit.fields.empty())
 			? MTPDinputSecureValue::Flag(0)
 			: MTPDinputSecureValue::Flag::f_data)
-		| (value->filesInEdit.empty()
+		| (value->scansInEdit.empty()
 			? MTPDinputSecureValue::Flag(0)
 			: MTPDinputSecureValue::Flag::f_files)
 		| ((value->selfieInEdit && !value->selfieInEdit->deleted)
@@ -954,27 +1120,15 @@ void FormController::sendSaveRequest(
 		data,
 		MTP_long(_secretId)
 	)).done([=](const MTPSecureValue &result) {
-		Expects(result.type() == mtpc_secureValue);
-
-		value->saveRequestId = 0;
-
-		const auto filesInEdit = base::take(value->filesInEdit);
-		auto selfiesInEdit = std::vector<EditFile>();
+		auto filesInEdit = base::take(value->scansInEdit);
 		if (auto selfie = base::take(value->selfieInEdit)) {
-			selfiesInEdit.push_back(std::move(*selfie));
+			filesInEdit.push_back(std::move(*selfie));
 		}
 
-		const auto &data = result.c_secureValue();
-		value->files = data.has_files()
-			? parseFiles(data.vfiles.v, filesInEdit)
-			: std::vector<File>();
-		value->selfie = data.has_selfie()
-			? parseFile(data.vselfie, selfiesInEdit)
-			: base::none;
-		value->data.encryptedSecret = std::move(
-			value->data.encryptedSecretInEdit);
-		value->data.parsed = std::move(value->data.parsedInEdit);
-		value->data.hash = std::move(value->data.hashInEdit);
+		const auto editScreens = value->editScreens;
+		*value = parseValue(result, filesInEdit);
+		decryptValue(*value);
+		value->editScreens = editScreens;
 
 		_valueSaveFinished.fire_copy(value);
 	}).fail([=](const RPCError &error) {
@@ -1167,12 +1321,15 @@ void FormController::generateSecret(bytes::const_span password) {
 }
 
 void FormController::requestForm() {
-	auto normalizedKey = _request.publicKey;
-	normalizedKey.replace("\r\n", "\n");
+	if (_request.payload.isEmpty()) {
+		_formRequestId = -1;
+		Ui::show(Box<InformBox>(lang(lng_passport_form_error)));
+		return;
+	}
 	_formRequestId = request(MTPaccount_GetAuthorizationForm(
 		MTP_int(_request.botId),
 		MTP_string(_request.scope),
-		MTP_bytes(normalizedKey.toUtf8())
+		MTP_string(_request.publicKey)
 	)).done([=](const MTPaccount_AuthorizationForm &result) {
 		_formRequestId = 0;
 		formDone(result);
@@ -1250,12 +1407,14 @@ void FormController::fillDownloadedFile(
 }
 
 auto FormController::parseValue(
-		const MTPSecureValue &value) const -> Value {
+		const MTPSecureValue &value,
+		const std::vector<EditFile> &editData) const -> Value {
 	Expects(value.type() == mtpc_secureValue);
 
 	const auto &data = value.c_secureValue();
 	const auto type = ConvertType(data.vtype);
 	auto result = Value(type);
+	result.submitHash = bytes::make_vector(data.vhash.v);
 	if (data.has_data()) {
 		Assert(data.vdata.type() == mtpc_secureData);
 		const auto &fields = data.vdata.c_secureData();
@@ -1264,10 +1423,10 @@ auto FormController::parseValue(
 		result.data.encryptedSecret = bytes::make_vector(fields.vsecret.v);
 	}
 	if (data.has_files()) {
-		result.files = parseFiles(data.vfiles.v);
+		result.scans = parseFiles(data.vfiles.v, editData);
 	}
 	if (data.has_selfie()) {
-		result.selfie = parseFile(data.vselfie);
+		result.selfie = parseFile(data.vselfie, editData);
 	}
 	if (data.has_plain_data()) {
 		switch (data.vplain_data.type()) {
@@ -1281,7 +1440,6 @@ auto FormController::parseValue(
 		} break;
 		}
 	}
-	// #TODO passport selfie
 	return result;
 }
 
@@ -1290,9 +1448,9 @@ auto FormController::findEditFile(const FullMsgId &fullId) -> EditFile* {
 		return (file.uploadData && file.uploadData->fullId == fullId);
 	};
 	for (auto &[type, value] : _form.values) {
-		for (auto &file : value.filesInEdit) {
-			if (found(file)) {
-				return &file;
+		for (auto &scan : value.scansInEdit) {
+			if (found(scan)) {
+				return &scan;
 			}
 		}
 		if (value.selfieInEdit && found(*value.selfieInEdit)) {
@@ -1307,9 +1465,9 @@ auto FormController::findEditFile(const FileKey &key) -> EditFile* {
 		return (file.fields.dcId == key.dcId && file.fields.id == key.id);
 	};
 	for (auto &[type, value] : _form.values) {
-		for (auto &file : value.filesInEdit) {
-			if (found(file)) {
-				return &file;
+		for (auto &scan : value.scansInEdit) {
+			if (found(scan)) {
+				return &scan;
 			}
 		}
 		if (value.selfieInEdit && found(*value.selfieInEdit)) {
@@ -1325,9 +1483,9 @@ auto FormController::findFile(const FileKey &key)
 		return (file.dcId == key.dcId) && (file.id == key.id);
 	};
 	for (auto &[type, value] : _form.values) {
-		for (auto &file : value.files) {
-			if (found(file)) {
-				return { &value, &file };
+		for (auto &scan : value.scans) {
+			if (found(scan)) {
+				return { &value, &scan };
 			}
 		}
 		if (value.selfie && found(*value.selfie)) {
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h
index 1c32e23f4..cbe79dd98 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_controller.h
@@ -31,12 +31,14 @@ struct FormRequest {
 		UserId botId,
 		const QString &scope,
 		const QString &callbackUrl,
-		const QString &publicKey);
+		const QString &publicKey,
+		const QString &payload);
 
 	UserId botId;
 	QString scope;
 	QString callbackUrl;
 	QString publicKey;
+	QString payload;
 
 };
 
@@ -142,11 +144,12 @@ struct Value {
 
 	Type type;
 	ValueData data;
-	std::vector<File> files;
-	std::vector<EditFile> filesInEdit;
+	std::vector<File> scans;
+	std::vector<EditFile> scansInEdit;
 	base::optional<File> selfie;
 	base::optional<EditFile> selfieInEdit;
 	Verification verification;
+	bytes::vector submitHash;
 
 	int editScreens = 0;
 	mtpRequestId saveRequestId = 0;
@@ -204,7 +207,7 @@ public:
 	void show();
 	UserData *bot() const;
 	QString privacyPolicyUrl() const;
-
+	void submit();
 	void submitPassword(const QString &password);
 	rpl::producer<QString> passwordError() const;
 	QString passwordHint() const;
@@ -223,6 +226,7 @@ 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);
@@ -244,6 +248,10 @@ public:
 	~FormController();
 
 private:
+	struct FinalData {
+		QVector<MTPSecureValueHash> hashes;
+		QByteArray credentials;
+	};
 	EditFile *findEditFile(const FullMsgId &fullId);
 	EditFile *findEditFile(const FileKey &key);
 	std::pair<Value*, File*> findFile(const FileKey &key);
@@ -256,13 +264,15 @@ private:
 	void formFail(const RPCError &error);
 	void parseForm(const MTPaccount_AuthorizationForm &result);
 	void showForm();
-	Value parseValue(const MTPSecureValue &value) const;
+	Value parseValue(
+		const MTPSecureValue &value,
+		const std::vector<EditFile> &editData = {}) const;
 	std::vector<File> parseFiles(
 		const QVector<MTPSecureFile> &data,
-		const std::vector<EditFile> &editData = {}) const;
+		const std::vector<EditFile> &editData) const;
 	base::optional<File> parseFile(
 		const MTPSecureFile &data,
-		const std::vector<EditFile> &editData = {}) const;
+		const std::vector<EditFile> &editData) const;
 	void fillDownloadedFile(
 		File &destination,
 		const std::vector<EditFile> &source) const;
@@ -330,6 +340,7 @@ private:
 	void sendSaveRequest(
 		not_null<Value*> value,
 		const MTPInputSecureValue &data);
+	FinalData prepareFinalData() const;
 
 	not_null<Window::Controller*> _controller;
 	FormRequest _request;
@@ -346,6 +357,7 @@ 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;
 
@@ -355,6 +367,7 @@ private:
 	mtpRequestId _saveSecretRequestId = 0;
 	rpl::event_stream<> _secretReady;
 	rpl::event_stream<QString> _passwordError;
+	mtpRequestId _submitRequestId = 0;
 
 	rpl::lifetime _uploaderSubscriptions;
 	rpl::lifetime _lifetime;
diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
index cb4472ce1..cac260eb1 100644
--- a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp
@@ -8,6 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "passport/passport_form_view_controller.h"
 
 #include "passport/passport_form_controller.h"
+#include "passport/passport_panel_edit_document.h"
+#include "passport/passport_panel_edit_contact.h"
+#include "passport/passport_panel_controller.h"
+#include "lang/lang_keys.h"
 
 namespace Passport {
 namespace {
@@ -57,7 +61,8 @@ Scope::Scope(Type type, not_null<const Value*> fields)
 , fields(fields) {
 }
 
-std::vector<Scope> ComputeScopes(not_null<FormController*> controller) {
+std::vector<Scope> ComputeScopes(
+		not_null<const FormController*> controller) {
 	auto scopes = std::map<Scope::Type, Scope>();
 	const auto &form = controller->form();
 	const auto findValue = [&](const Value::Type type) {
@@ -74,15 +79,15 @@ std::vector<Scope> ComputeScopes(not_null<FormController*> controller) {
 		i->second.selfieRequired = (scopeType == Scope::Type::Identity)
 			&& form.identitySelfieRequired;
 		const auto alreadyIt = ranges::find(
-			i->second.files,
+			i->second.documents,
 			type,
 			[](not_null<const Value*> value) { return value->type; });
-		if (alreadyIt != end(i->second.files)) {
+		if (alreadyIt != end(i->second.documents)) {
 			LOG(("API Error: Value type %1 multiple times in request."
 				).arg(int(type)));
 			continue;
 		} else if (type != fieldsType) {
-			i->second.files.push_back(findValue(type));
+			i->second.documents.push_back(findValue(type));
 		}
 	}
 	auto result = std::vector<Scope>();
@@ -93,4 +98,168 @@ std::vector<Scope> ComputeScopes(not_null<FormController*> controller) {
 	return result;
 }
 
+QString ComputeScopeRowReadyString(const Scope &scope) {
+	switch (scope.type) {
+	case Scope::Type::Identity:
+	case Scope::Type::Address: {
+		auto list = QStringList();
+		const auto &fields = scope.fields->data.parsed.fields;
+		const auto document = [&]() -> const Value* {
+			for (const auto &document : scope.documents) {
+				if (!document->scans.empty()) {
+					return document;
+				}
+			}
+			return nullptr;
+		}();
+		if (document && scope.documents.size() > 1) {
+			list.push_back([&] {
+				switch (document->type) {
+				case Value::Type::Passport:
+					return lang(lng_passport_identity_passport);
+				case Value::Type::DriverLicense:
+					return lang(lng_passport_identity_license);
+				case Value::Type::IdentityCard:
+					return lang(lng_passport_identity_card);
+				case Value::Type::BankStatement:
+					return lang(lng_passport_address_statement);
+				case Value::Type::UtilityBill:
+					return lang(lng_passport_address_bill);
+				case Value::Type::RentalAgreement:
+					return lang(lng_passport_address_agreement);
+				default: Unexpected("Files type in ComputeScopeRowReadyString.");
+				}
+			}());
+		}
+		if (document
+			&& (document->scans.empty()
+				|| (scope.selfieRequired && !document->selfie))) {
+			return QString();
+		}
+		const auto scheme = GetDocumentScheme(scope.type);
+		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)) {
+					return QString();
+				} else if (row.validate && !row.validate(i->second)) {
+					return QString();
+				}
+				list.push_back(format ? format(i->second) : i->second);
+			} else if (!document) {
+				return QString();
+			} else {
+				const auto i = document->data.parsed.fields.find(row.key);
+				if (i == end(document->data.parsed.fields)) {
+					return QString();
+				} else if (row.validate && !row.validate(i->second)) {
+					return QString();
+				}
+				list.push_back(i->second);
+			}
+		}
+		return list.join(", ");
+	} break;
+	case Scope::Type::Phone:
+	case Scope::Type::Email: {
+		const auto format = GetContactScheme(scope.type).format;
+		const auto &fields = scope.fields->data.parsed.fields;
+		const auto i = fields.find("value");
+		return (i != end(fields))
+			? (format ? format(i->second) : i->second)
+			: QString();
+	} break;
+	}
+	Unexpected("Scope type in ComputeScopeRowReadyString.");
+}
+
+ScopeRow ComputeScopeRow(const Scope &scope) {
+	switch (scope.type) {
+	case Scope::Type::Identity:
+		if (scope.documents.empty()) {
+			return {
+				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 {
+					lang(lng_passport_identity_passport),
+					lang(lng_passport_identity_passport_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			case Value::Type::IdentityCard:
+				return {
+					lang(lng_passport_identity_card),
+					lang(lng_passport_identity_card_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			case Value::Type::DriverLicense:
+				return {
+					lang(lng_passport_identity_license),
+					lang(lng_passport_identity_license_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			default: Unexpected("Identity type in ComputeScopeRow.");
+			}
+		}
+		return {
+			lang(lng_passport_identity_title),
+			lang(lng_passport_identity_description),
+			ComputeScopeRowReadyString(scope)
+		};
+	case Scope::Type::Address:
+		if (scope.documents.empty()) {
+			return {
+				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 {
+					lang(lng_passport_address_statement),
+					lang(lng_passport_address_statement_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			case Value::Type::UtilityBill:
+				return {
+					lang(lng_passport_address_bill),
+					lang(lng_passport_address_bill_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			case Value::Type::RentalAgreement:
+				return {
+					lang(lng_passport_address_agreement),
+					lang(lng_passport_address_agreement_upload),
+					ComputeScopeRowReadyString(scope)
+				};
+			default: Unexpected("Address type in ComputeScopeRow.");
+			}
+		}
+		return {
+			lang(lng_passport_address_title),
+			lang(lng_passport_address_description),
+			ComputeScopeRowReadyString(scope)
+		};
+	case Scope::Type::Phone:
+		return {
+			lang(lng_passport_phone_title),
+			lang(lng_passport_phone_description),
+			ComputeScopeRowReadyString(scope)
+		};
+	case Scope::Type::Email:
+		return {
+			lang(lng_passport_email_title),
+			lang(lng_passport_email_description),
+			ComputeScopeRowReadyString(scope)
+		};
+	default: Unexpected("Scope type in ComputeScopeRow.");
+	}
+}
+
 } // namespace Passport
diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h
index 7b4a8adff..e5f8a18fc 100644
--- a/Telegram/SourceFiles/passport/passport_form_view_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h
@@ -22,11 +22,20 @@ struct Scope {
 
 	Type type;
 	not_null<const Value*> fields;
-	std::vector<not_null<const Value*>> files;
+	std::vector<not_null<const Value*>> documents;
 	bool selfieRequired = false;
 };
 
-std::vector<Scope> ComputeScopes(not_null<FormController*> controller);
+struct ScopeRow {
+	QString title;
+	QString description;
+	QString ready;
+};
+
+std::vector<Scope> ComputeScopes(
+	not_null<const FormController*> controller);
+QString ComputeScopeRowReadyString(const Scope &scope);
+ScopeRow ComputeScopeRow(const Scope &scope);
 
 class ViewController {
 public:
diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
index f2f97d358..ac1a68d46 100644
--- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
@@ -18,13 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "layout.h"
 
 namespace Passport {
-namespace {
 
-PanelEditDocument::Scheme GetDocumentScheme(
+EditDocumentScheme GetDocumentScheme(
 		Scope::Type type,
-		base::optional<Value::Type> scansType = base::none) {
-	using Scheme = PanelEditDocument::Scheme;
-
+		base::optional<Value::Type> scansType) {
+	using Scheme = EditDocumentScheme;
+	using ValueClass = Scheme::ValueClass;
 	const auto DontFormat = nullptr;
 	const auto CountryFormat = [](const QString &value) {
 		const auto result = CountrySelectBox::NameByISO(value);
@@ -78,7 +77,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 		}
 		result.rows = {
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("first_name"),
 				lang(lng_passport_first_name),
@@ -86,7 +85,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("last_name"),
 				lang(lng_passport_last_name),
@@ -94,7 +93,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Date,
 				qsl("birth_date"),
 				lang(lng_passport_birth_date),
@@ -102,7 +101,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Gender,
 				qsl("gender"),
 				lang(lng_passport_gender),
@@ -110,7 +109,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				GenderFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Country,
 				qsl("country_code"),
 				lang(lng_passport_country),
@@ -118,7 +117,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				CountryFormat,
 			},
 			{
-				Scheme::ValueType::Scans,
+				ValueClass::Scans,
 				PanelDetailsType::Text,
 				qsl("document_no"),
 				lang(lng_passport_document_number),
@@ -126,7 +125,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Scans,
+				ValueClass::Scans,
 				PanelDetailsType::Date,
 				qsl("expiry_date"),
 				lang(lng_passport_expiry_date),
@@ -157,7 +156,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 		}
 		result.rows = {
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("street_line1"),
 				lang(lng_passport_street),
@@ -165,7 +164,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("street_line2"),
 				lang(lng_passport_street),
@@ -173,7 +172,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("city"),
 				lang(lng_passport_city),
@@ -181,7 +180,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("state"),
 				lang(lng_passport_state),
@@ -189,7 +188,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				DontFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Country,
 				qsl("country_code"),
 				lang(lng_passport_country),
@@ -197,7 +196,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
 				CountryFormat,
 			},
 			{
-				Scheme::ValueType::Fields,
+				ValueClass::Fields,
 				PanelDetailsType::Text,
 				qsl("post_code"),
 				lang(lng_passport_postcode),
@@ -211,11 +210,13 @@ PanelEditDocument::Scheme GetDocumentScheme(
 	Unexpected("Type in GetDocumentScheme().");
 }
 
-PanelEditContact::Scheme GetContactScheme(Scope::Type type) {
-	using Scheme = PanelEditContact::Scheme;
+EditContactScheme GetContactScheme(Scope::Type type) {
+	using Scheme = EditContactScheme;
+	using ValueType = Scheme::ValueType;
+
 	switch (type) {
 	case Scope::Type::Phone: {
-		auto result = Scheme(Scheme::ValueType::Phone);
+		auto result = Scheme(ValueType::Phone);
 		result.aboutExisting = lang(lng_passport_use_existing_phone);
 		result.newHeader = lang(lng_passport_new_phone);
 		result.aboutNew = lang(lng_passport_new_phone_code);
@@ -234,7 +235,7 @@ PanelEditContact::Scheme GetContactScheme(Scope::Type type) {
 	} break;
 
 	case Scope::Type::Email: {
-		auto result = Scheme(Scheme::ValueType::Text);
+		auto result = Scheme(ValueType::Text);
 		result.aboutExisting = lang(lng_passport_use_existing_email);
 		result.newHeader = lang(lng_passport_new_email);
 		result.newPlaceholder = langFactory(lng_passport_email_title);
@@ -253,8 +254,6 @@ PanelEditContact::Scheme GetContactScheme(Scope::Type type) {
 	Unexpected("Type in GetContactScheme().");
 }
 
-} // namespace
-
 BoxPointer::BoxPointer(QPointer<BoxContent> value)
 : _value(value) {
 }
@@ -323,158 +322,6 @@ QString PanelController::privacyPolicyUrl() const {
 	return _form->privacyPolicyUrl();
 }
 
-auto PanelController::collectRowInfo(const Scope &scope) const -> Row {
-	switch (scope.type) {
-	case Scope::Type::Identity:
-		if (scope.files.empty()) {
-			return {
-				lang(lng_passport_personal_details),
-				lang(lng_passport_personal_details_enter)
-			};
-		} else if (scope.files.size() == 1) {
-			switch (scope.files.front()->type) {
-			case Value::Type::Passport:
-				return {
-					lang(lng_passport_identity_passport),
-					lang(lng_passport_identity_passport_upload)
-				};
-			case Value::Type::IdentityCard:
-				return {
-					lang(lng_passport_identity_card),
-					lang(lng_passport_identity_card_upload)
-				};
-			case Value::Type::DriverLicense:
-				return {
-					lang(lng_passport_identity_license),
-					lang(lng_passport_identity_license_upload)
-				};
-			default: Unexpected("Identity type in collectRowInfo.");
-			}
-		}
-		return {
-			lang(lng_passport_identity_title),
-			lang(lng_passport_identity_description)
-		};
-	case Scope::Type::Address:
-		if (scope.files.empty()) {
-			return {
-				lang(lng_passport_address),
-				lang(lng_passport_address_enter)
-			};
-		} else if (scope.files.size() == 1) {
-			switch (scope.files.front()->type) {
-			case Value::Type::BankStatement:
-				return {
-					lang(lng_passport_address_statement),
-					lang(lng_passport_address_statement_upload)
-				};
-			case Value::Type::UtilityBill:
-				return {
-					lang(lng_passport_address_bill),
-					lang(lng_passport_address_bill_upload)
-				};
-			case Value::Type::RentalAgreement:
-				return {
-					lang(lng_passport_address_agreement),
-					lang(lng_passport_address_agreement_upload)
-				};
-			default: Unexpected("Address type in collectRowInfo.");
-			}
-		}
-		return {
-			lang(lng_passport_address_title),
-			lang(lng_passport_address_description)
-		};
-	case Scope::Type::Phone:
-		return {
-			lang(lng_passport_phone_title),
-			lang(lng_passport_phone_description)
-		};
-	case Scope::Type::Email:
-		return {
-			lang(lng_passport_email_title),
-			lang(lng_passport_email_description)
-		};
-	default: Unexpected("Scope type in collectRowInfo.");
-	}
-}
-
-QString PanelController::collectRowReadyString(const Scope &scope) const {
-	switch (scope.type) {
-	case Scope::Type::Identity:
-	case Scope::Type::Address: {
-		auto list = QStringList();
-		const auto &fields = scope.fields->data.parsed.fields;
-		const auto files = [&]() -> const Value* {
-			for (const auto &files : scope.files) {
-				if (!files->files.empty()) {
-					return files;
-				}
-			}
-			return nullptr;
-		}();
-		if (files && scope.files.size() > 1) {
-			list.push_back([&] {
-				switch (files->type) {
-				case Value::Type::Passport:
-					return lang(lng_passport_identity_passport);
-				case Value::Type::DriverLicense:
-					return lang(lng_passport_identity_license);
-				case Value::Type::IdentityCard:
-					return lang(lng_passport_identity_card);
-				case Value::Type::BankStatement:
-					return lang(lng_passport_address_statement);
-				case Value::Type::UtilityBill:
-					return lang(lng_passport_address_bill);
-				case Value::Type::RentalAgreement:
-					return lang(lng_passport_address_agreement);
-				default: Unexpected("Files type in collectRowReadyString.");
-				}
-			}());
-		}
-		if (files
-			&& (files->files.empty()
-				|| (scope.selfieRequired && !files->selfie))) {
-			return QString();
-		}
-		const auto scheme = GetDocumentScheme(scope.type);
-		for (const auto &row : scheme.rows) {
-			const auto format = row.format;
-			if (row.type == PanelEditDocument::Scheme::ValueType::Fields) {
-				const auto i = fields.find(row.key);
-				if (i == end(fields)) {
-					return QString();
-				} else if (row.validate && !row.validate(i->second)) {
-					return QString();
-				}
-				list.push_back(format ? format(i->second) : i->second);
-			} else if (!files) {
-				return QString();
-			} else {
-				const auto i = files->data.parsed.fields.find(row.key);
-				if (i == end(files->data.parsed.fields)) {
-					return QString();
-				} else if (row.validate && !row.validate(i->second)) {
-					return QString();
-				}
-				list.push_back(i->second);
-			}
-		}
-		return list.join(", ");
-	} break;
-	case Scope::Type::Phone:
-	case Scope::Type::Email: {
-		const auto format = GetContactScheme(scope.type).format;
-		const auto &fields = scope.fields->data.parsed.fields;
-		const auto i = fields.find("value");
-		return (i != end(fields))
-			? (format ? format(i->second) : i->second)
-			: QString();
-	} break;
-	}
-	Unexpected("Scope type in collectRowReadyString.");
-}
-
 void PanelController::fillRows(
 	base::lambda<void(
 		QString title,
@@ -484,15 +331,18 @@ void PanelController::fillRows(
 		_scopes = ComputeScopes(_form);
 	}
 	for (const auto &scope : _scopes) {
-		const auto row = collectRowInfo(scope);
-		const auto ready = collectRowReadyString(scope);
+		const auto row = ComputeScopeRow(scope);
 		callback(
 			row.title,
-			ready.isEmpty() ? row.description : ready,
-			!ready.isEmpty());
+			row.ready.isEmpty() ? row.description : row.ready,
+			!row.ready.isEmpty());
 	}
 }
 
+void PanelController::submitForm() {
+	_form->submit();
+}
+
 void PanelController::submitPassword(const QString &password) {
 	_form->submitPassword(password);
 }
@@ -515,71 +365,71 @@ QString PanelController::defaultPhoneNumber() const {
 
 void PanelController::uploadScan(QByteArray &&content) {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 
 	_form->uploadScan(
-		_editScope->files[_editScopeFilesIndex],
+		_editScope->documents[_editDocumentIndex],
 		std::move(content));
 }
 
 void PanelController::deleteScan(int fileIndex) {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 
 	_form->deleteScan(
-		_editScope->files[_editScopeFilesIndex],
+		_editScope->documents[_editDocumentIndex],
 		fileIndex);
 }
 
 void PanelController::restoreScan(int fileIndex) {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 
 	_form->restoreScan(
-		_editScope->files[_editScopeFilesIndex],
+		_editScope->documents[_editDocumentIndex],
 		fileIndex);
 }
 
 void PanelController::uploadSelfie(QByteArray &&content) {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 	Expects(_editScope->selfieRequired);
 
 	_form->uploadSelfie(
-		_editScope->files[_editScopeFilesIndex],
+		_editScope->documents[_editDocumentIndex],
 		std::move(content));
 }
 
 void PanelController::deleteSelfie() {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 	Expects(_editScope->selfieRequired);
 
 	_form->deleteSelfie(
-		_editScope->files[_editScopeFilesIndex]);
+		_editScope->documents[_editDocumentIndex]);
 }
 
 void PanelController::restoreSelfie() {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0
-		&& _editScopeFilesIndex < _editScope->files.size());
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
 	Expects(_editScope->selfieRequired);
 
 	_form->restoreSelfie(
-		_editScope->files[_editScopeFilesIndex]);
+		_editScope->documents[_editDocumentIndex]);
 }
 
 rpl::producer<ScanInfo> PanelController::scanUpdated() const {
 	return _form->scanUpdated(
 	) | rpl::filter([=](not_null<const EditFile*> file) {
 		return (_editScope != nullptr)
-			&& (_editScopeFilesIndex >= 0)
-			&& (file->value == _editScope->files[_editScopeFilesIndex]);
+			&& (_editDocumentIndex >= 0)
+			&& (file->value == _editScope->documents[_editDocumentIndex]);
 	}) | rpl::map([=](not_null<const EditFile*> file) {
 		return collectScanInfo(*file);
 	});
@@ -587,7 +437,7 @@ rpl::producer<ScanInfo> PanelController::scanUpdated() const {
 
 ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
 	Expects(_editScope != nullptr);
-	Expects(_editScopeFilesIndex >= 0);
+	Expects(_editDocumentIndex >= 0);
 
 	const auto status = [&] {
 		if (file.fields.accessHash) {
@@ -618,10 +468,10 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
 			return formatDownloadText(0, file.fields.size);
 		}
 	}();
-	const auto &files = _editScope->files;
-	auto isSelfie = (file.value == files[_editScopeFilesIndex])
-		&& (files[_editScopeFilesIndex]->selfieInEdit.has_value())
-		&& (&file == &*files[_editScopeFilesIndex]->selfieInEdit);
+	const auto &documents = _editScope->documents;
+	auto isSelfie = (file.value == documents[_editDocumentIndex])
+		&& (documents[_editDocumentIndex]->selfieInEdit.has_value())
+		&& (&file == &*documents[_editDocumentIndex]->selfieInEdit);
 	return {
 		FileKey{ file.fields.id, file.fields.dcId },
 		status,
@@ -664,7 +514,7 @@ void PanelController::ensurePanelCreated() {
 int PanelController::findNonEmptyIndex(
 		const std::vector<not_null<const Value*>> &files) const {
 	const auto i = ranges::find_if(files, [](not_null<const Value*> file) {
-		return !file->files.empty();
+		return !file->scans.empty();
 	});
 	if (i != end(files)) {
 		return (i - begin(files));
@@ -677,13 +527,14 @@ void PanelController::editScope(int index) {
 	Expects(_panel != nullptr);
 	Expects(index >= 0 && index < _scopes.size());
 
-	if (_scopes[index].files.empty()) {
+	if (_scopes[index].documents.empty()) {
 		editScope(index, -1);
 	} else {
-		const auto filesIndex = findNonEmptyIndex(_scopes[index].files);
-		if (filesIndex >= 0) {
-			editScope(index, filesIndex);
-		} else if (_scopes[index].files.size() > 1) {
+		const auto documentIndex = findNonEmptyIndex(
+			_scopes[index].documents);
+		if (documentIndex >= 0) {
+			editScope(index, documentIndex);
+		} else if (_scopes[index].documents.size() > 1) {
 			requestScopeFilesType(index);
 		} else {
 			editWithUpload(index, 0);
@@ -696,14 +547,14 @@ void PanelController::requestScopeFilesType(int index) {
 	Expects(index >= 0 && index < _scopes.size());
 
 	const auto type = _scopes[index].type;
-	_scopeFilesTypeBox = [&] {
+	_scopeDocumentTypeBox = [&] {
 		if (type == Scope::Type::Identity) {
 			return show(RequestIdentityType(
-				[=](int filesIndex) {
-					editWithUpload(index, filesIndex);
+				[=](int documentIndex) {
+					editWithUpload(index, documentIndex);
 				},
 				ranges::view::all(
-					_scopes[index].files
+					_scopes[index].documents
 				) | ranges::view::transform([](auto value) {
 					return value->type;
 				}) | ranges::view::transform([](Value::Type type) {
@@ -720,11 +571,11 @@ void PanelController::requestScopeFilesType(int index) {
 				}) | ranges::to_vector));
 		} else if (type == Scope::Type::Address) {
 			return show(RequestAddressType(
-				[=](int filesIndex) {
-					editWithUpload(index, filesIndex);
+				[=](int documentIndex) {
+					editWithUpload(index, documentIndex);
 				},
 				ranges::view::all(
-					_scopes[index].files
+					_scopes[index].documents
 				) | ranges::view::transform([](auto value) {
 					return value->type;
 				}) | ranges::view::transform([](Value::Type type) {
@@ -745,51 +596,53 @@ void PanelController::requestScopeFilesType(int index) {
 	}();
 }
 
-void PanelController::editWithUpload(int index, int filesIndex) {
+void PanelController::editWithUpload(int index, int documentIndex) {
 	Expects(_panel != nullptr);
 	Expects(index >= 0 && index < _scopes.size());
-	Expects(filesIndex >= 0 && filesIndex < _scopes[index].files.size());
+	Expects(documentIndex >= 0
+		&& documentIndex < _scopes[index].documents.size());
 
 	EditScans::ChooseScan(
 		base::lambda_guarded(_panel.get(),
 		[=](QByteArray &&content) {
-			base::take(_scopeFilesTypeBox);
-			editScope(index, filesIndex);
+			base::take(_scopeDocumentTypeBox);
+			editScope(index, documentIndex);
 			uploadScan(std::move(content));
 		}));
 }
 
-void PanelController::editScope(int index, int filesIndex) {
+void PanelController::editScope(int index, int documentIndex) {
 	Expects(_panel != nullptr);
 	Expects(index >= 0 && index < _scopes.size());
-	Expects((filesIndex < 0)
-		|| (filesIndex >= 0 && filesIndex < _scopes[index].files.size()));
+	Expects((documentIndex < 0)
+		|| (documentIndex >= 0
+			&& documentIndex < _scopes[index].documents.size()));
 
 	_editScope = &_scopes[index];
-	_editScopeFilesIndex = filesIndex;
+	_editDocumentIndex = documentIndex;
 
 	_form->startValueEdit(_editScope->fields);
-	if (_editScopeFilesIndex >= 0) {
-		_form->startValueEdit(_editScope->files[_editScopeFilesIndex]);
+	if (_editDocumentIndex >= 0) {
+		_form->startValueEdit(_editScope->documents[_editDocumentIndex]);
 	}
 
 	auto content = [&]() -> object_ptr<Ui::RpWidget> {
 		switch (_editScope->type) {
 		case Scope::Type::Identity:
 		case Scope::Type::Address: {
-			const auto &files = _editScope->files;
-			auto result = (_editScopeFilesIndex >= 0)
+			const auto &documents = _editScope->documents;
+			auto result = (_editDocumentIndex >= 0)
 				? object_ptr<PanelEditDocument>(
 					_panel.get(),
 					this,
 					GetDocumentScheme(
 						_editScope->type,
-						files[_editScopeFilesIndex]->type),
+						documents[_editDocumentIndex]->type),
 					_editScope->fields->data.parsedInEdit,
-					files[_editScopeFilesIndex]->data.parsedInEdit,
-					valueFiles(*files[_editScopeFilesIndex]),
+					documents[_editDocumentIndex]->data.parsedInEdit,
+					valueFiles(*documents[_editDocumentIndex]),
 					(_editScope->selfieRequired
-						? valueSelfie(*files[_editScopeFilesIndex])
+						? valueSelfie(*documents[_editDocumentIndex])
 						: nullptr))
 				: object_ptr<PanelEditDocument>(
 					_panel.get(),
@@ -850,8 +703,8 @@ void PanelController::processValueSaveFinished(
 	}
 
 	const auto value1 = _editScope->fields;
-	const auto value2 = (_editScopeFilesIndex >= 0)
-		? _editScope->files[_editScopeFilesIndex].get()
+	const auto value2 = (_editDocumentIndex >= 0)
+		? _editScope->documents[_editDocumentIndex].get()
 		: nullptr;
 	if (value == value1 || value == value2) {
 		if (!_form->savingValue(value1)
@@ -925,8 +778,8 @@ void PanelController::processVerificationNeeded(
 std::vector<ScanInfo> PanelController::valueFiles(
 		const Value &value) const {
 	auto result = std::vector<ScanInfo>();
-	for (const auto &file : value.filesInEdit) {
-		result.push_back(collectScanInfo(file));
+	for (const auto &scan : value.scansInEdit) {
+		result.push_back(collectScanInfo(scan));
 	}
 	return result;
 }
@@ -943,9 +796,9 @@ std::unique_ptr<ScanInfo> PanelController::valueSelfie(
 void PanelController::cancelValueEdit() {
 	if (const auto scope = base::take(_editScope)) {
 		_form->cancelValueEdit(scope->fields);
-		const auto index = std::exchange(_editScopeFilesIndex, -1);
+		const auto index = std::exchange(_editDocumentIndex, -1);
 		if (index >= 0) {
-			_form->cancelValueEdit(scope->files[index]);
+			_form->cancelValueEdit(scope->documents[index]);
 		}
 	}
 }
@@ -955,9 +808,9 @@ void PanelController::saveScope(ValueMap &&data, ValueMap &&filesData) {
 	Expects(_editScope != nullptr);
 
 	_form->saveValueEdit(_editScope->fields, std::move(data));
-	if (_editScopeFilesIndex >= 0) {
+	if (_editDocumentIndex >= 0) {
 		_form->saveValueEdit(
-			_editScope->files[_editScopeFilesIndex],
+			_editScope->documents[_editDocumentIndex],
 			std::move(filesData));
 	} else {
 		Assert(filesData.fields.empty());
@@ -971,9 +824,9 @@ bool PanelController::editScopeChanged(
 
 	if (_form->editValueChanged(_editScope->fields, data)) {
 		return true;
-	} else if (_editScopeFilesIndex >= 0) {
+	} else if (_editDocumentIndex >= 0) {
 		return _form->editValueChanged(
-			_editScope->files[_editScopeFilesIndex],
+			_editScope->documents[_editDocumentIndex],
 			filesData);
 	}
 	return false;
diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h
index 7d1c8c0bd..4f01f4077 100644
--- a/Telegram/SourceFiles/passport/passport_panel_controller.h
+++ b/Telegram/SourceFiles/passport/passport_panel_controller.h
@@ -15,6 +15,14 @@ namespace Passport {
 class FormController;
 class Panel;
 
+struct EditDocumentScheme;
+struct EditContactScheme;
+
+EditDocumentScheme GetDocumentScheme(
+	Scope::Type type,
+	base::optional<Value::Type> scansType = base::none);
+EditContactScheme GetContactScheme(Scope::Type type);
+
 struct ScanInfo {
 	FileKey key;
 	QString status;
@@ -47,7 +55,7 @@ public:
 
 	not_null<UserData*> bot() const;
 	QString privacyPolicyUrl() const;
-
+	void submitForm();
 	void submitPassword(const QString &password);
 	rpl::producer<QString> passwordError() const;
 	QString passwordHint() const;
@@ -87,14 +95,10 @@ public:
 	rpl::lifetime &lifetime();
 
 private:
-	struct Row {
-		QString title;
-		QString description;
-	};
 	void ensurePanelCreated();
 
-	void editScope(int index, int filesIndex);
-	void editWithUpload(int index, int filesIndex);
+	void editScope(int index, int documentIndex);
+	void editWithUpload(int index, int documentIndex);
 	int findNonEmptyIndex(
 		const std::vector<not_null<const Value*>> &files) const;
 	void requestScopeFilesType(int index);
@@ -104,8 +108,6 @@ private:
 	void processValueSaveFinished(not_null<const Value*> value);
 	void processVerificationNeeded(not_null<const Value*> value);
 
-	Row collectRowInfo(const Scope &scope) const;
-	QString collectRowReadyString(const Scope &scope) const;
 	ScanInfo collectScanInfo(const EditFile &file) const;
 	QString getDefaultContactValue(Scope::Type type) const;
 
@@ -116,8 +118,8 @@ private:
 	base::lambda<bool()> _panelHasUnsavedChanges;
 	BoxPointer _confirmForgetChangesBox;
 	Scope *_editScope = nullptr;
-	int _editScopeFilesIndex = -1;
-	BoxPointer _scopeFilesTypeBox;
+	int _editDocumentIndex = -1;
+	BoxPointer _scopeDocumentTypeBox;
 	std::map<not_null<const Value*>, BoxPointer> _verificationBoxes;
 
 	rpl::lifetime _lifetime;
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp
index 24c23f61f..9a8098a9b 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp
@@ -159,7 +159,7 @@ void VerifyBox::prepare() {
 
 } // namespace
 
-PanelEditContact::Scheme::Scheme(ValueType type) : type(type) {
+EditContactScheme::EditContactScheme(ValueType type) : type(type) {
 }
 
 PanelEditContact::PanelEditContact(
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.h b/Telegram/SourceFiles/passport/passport_panel_edit_contact.h
index f7fd910a4..3d9f8bd8b 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.h
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.h
@@ -20,26 +20,28 @@ namespace Passport {
 
 class PanelController;
 
+struct EditContactScheme {
+	enum class ValueType {
+		Phone,
+		Text,
+	};
+	explicit EditContactScheme(ValueType type);
+
+	ValueType type;
+
+	QString aboutExisting;
+	QString newHeader;
+	base::lambda<QString()> newPlaceholder;
+	QString aboutNew;
+	base::lambda<bool(const QString &value)> validate;
+	base::lambda<QString(const QString &value)> format;
+	base::lambda<QString(const QString &value)> postprocess;
+
+};
+
 class PanelEditContact : public Ui::RpWidget {
 public:
-	struct Scheme {
-		enum class ValueType {
-			Phone,
-			Text,
-		};
-		explicit Scheme(ValueType type);
-
-		ValueType type;
-
-		QString aboutExisting;
-		QString newHeader;
-		base::lambda<QString()> newPlaceholder;
-		QString aboutNew;
-		base::lambda<bool(const QString &value)> validate;
-		base::lambda<QString(const QString &value)> format;
-		base::lambda<QString(const QString &value)> postprocess;
-
-	};
+	using Scheme = EditContactScheme;
 
 	PanelEditContact(
 		QWidget *parent,
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
index 013648053..cb7c197ca 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
@@ -234,7 +234,7 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
 
 	for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
 		const auto &row = _scheme.rows[i];
-		auto fields = (row.type == Scheme::ValueType::Fields)
+		auto fields = (row.valueClass == Scheme::ValueClass::Fields)
 			? &data
 			: scanData;
 		if (!fields) {
@@ -289,7 +289,7 @@ PanelEditDocument::Result PanelEditDocument::collect() const {
 	auto result = Result();
 	for (const auto [i, field] : _details) {
 		const auto &row = _scheme.rows[i];
-		auto &fields = (row.type == Scheme::ValueType::Fields)
+		auto &fields = (row.valueClass == Scheme::ValueClass::Fields)
 			? result.data
 			: result.filesData;
 		fields.fields[row.key] = field->valueCurrent();
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h
index 9e5c9f7a0..0ff4f1c42 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h
@@ -26,26 +26,28 @@ class EditScans;
 class PanelDetailsRow;
 enum class PanelDetailsType;
 
+struct EditDocumentScheme {
+	enum class ValueClass {
+		Fields,
+		Scans,
+	};
+	struct Row {
+		ValueClass valueClass = ValueClass::Fields;
+		PanelDetailsType inputType = PanelDetailsType();
+		QString key;
+		QString label;
+		base::lambda<bool(const QString &value)> validate;
+		base::lambda<QString(const QString &value)> format;
+	};
+	std::vector<Row> rows;
+	QString rowsHeader;
+	QString scansHeader;
+
+};
+
 class PanelEditDocument : public Ui::RpWidget {
 public:
-	struct Scheme {
-		enum class ValueType {
-			Fields,
-			Scans,
-		};
-		struct Row {
-			ValueType type = ValueType::Fields;
-			PanelDetailsType inputType = PanelDetailsType();
-			QString key;
-			QString label;
-			base::lambda<bool(const QString &value)> validate;
-			base::lambda<QString(const QString &value)> format;
-		};
-		std::vector<Row> rows;
-		QString rowsHeader;
-		QString scansHeader;
-
-	};
+	using Scheme = EditDocumentScheme;
 
 	PanelEditDocument(
 		QWidget *parent,
diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp
index 1f30ccff6..21ebbb01c 100644
--- a/Telegram/SourceFiles/passport/passport_panel_form.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp
@@ -145,6 +145,10 @@ PanelForm::PanelForm(
 void PanelForm::setupControls() {
 	const auto inner = setupContent();
 
+	_submit->addClickHandler([=] {
+		_controller->submitForm();
+	});
+
 	using namespace rpl::mappers;
 
 	_topShadow->toggleOn(