Submit passport results.

This commit is contained in:
John Preston 2018-04-12 19:45:04 +04:00
parent c20cf243db
commit ead31757d7
18 changed files with 664 additions and 403 deletions

View file

@ -44,4 +44,16 @@ bool is_ipv6(const QString &ip) {
return ip.indexOf('.') < 0 && ip.indexOf(':') >= 0; 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 } // namespace qthelp

View file

@ -22,7 +22,11 @@ enum class UrlParamNameTransform {
ToLower, ToLower,
}; };
// Parses a string like "p1=v1&p2=v2&..&pn=vn" to a map. // 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); bool is_ipv6(const QString &ip);

View file

@ -841,13 +841,15 @@ bool Messenger::openLocalUrl(const QString &url) {
const auto scope = params.value("scope", QString()); const auto scope = params.value("scope", QString());
const auto callback = params.value("callback_url", QString()); const auto callback = params.value("callback_url", QString());
const auto publicKey = params.value("public_key", 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 window = App::wnd()) {
if (const auto controller = window->controller()) { if (const auto controller = window->controller()) {
controller->showPassportForm(Passport::FormRequest( controller->showPassportForm(Passport::FormRequest(
botId, botId,
scope, scope,
callback, callback,
publicKey)); publicKey,
payload));
return true; return true;
} }
} }

View file

@ -164,6 +164,29 @@ public:
} }
return result; 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() { ~Private() {
RSA_free(_rsa); RSA_free(_rsa);
} }
@ -236,5 +259,10 @@ bytes::vector RSAPublicKey::decrypt(bytes::const_span data) const {
return _private->decrypt(data); return _private->decrypt(data);
} }
bytes::vector RSAPublicKey::encryptOAEPpadding(
bytes::const_span data) const {
return _private->encryptOAEPpadding(data);
}
} // namespace internal } // namespace internal
} // namespace MTP } // namespace MTP

View file

@ -37,6 +37,9 @@ public:
// data has exactly 256 chars to be decrypted // data has exactly 256 chars to be decrypted
bytes::vector decrypt(bytes::const_span data) const; 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: private:
class Private; class Private;
std::shared_ptr<Private> _private; std::shared_ptr<Private> _private;

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_encryption.h" #include "passport/passport_encryption.h"
#include "base/openssl_help.h" #include "base/openssl_help.h"
#include "mtproto/rsa_public_key.h"
namespace Passport { namespace Passport {
namespace { namespace {
@ -314,14 +315,6 @@ bytes::vector PrepareValueHash(
return openssl::Sha256(bytes::concatenate(dataHash, valueSecret)); 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::vector EncryptValueSecret(
bytes::const_span valueSecret, bytes::const_span valueSecret,
bytes::const_span secret, bytes::const_span secret,
@ -350,4 +343,11 @@ uint64 CountSecureSecretHash(bytes::const_span secret) {
return *reinterpret_cast<const uint64*>(part.data()); 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 } // namespace Passport

View file

@ -54,10 +54,10 @@ bytes::vector DecryptValueSecret(
bytes::const_span secret, bytes::const_span secret,
bytes::const_span valueHash); bytes::const_span valueHash);
bytes::vector PrepareFilesHash(
gsl::span<bytes::const_span> fileHashes,
bytes::const_span valueSecret);
uint64 CountSecureSecretHash(bytes::const_span secret); uint64 CountSecureSecretHash(bytes::const_span secret);
bytes::vector EncryptCredentialsSecret(
bytes::const_span secret,
bytes::const_span publicKey);
} // namespace Passport } // namespace Passport

View file

@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/confirm_box.h" #include "boxes/confirm_box.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "base/openssl_help.h" #include "base/openssl_help.h"
#include "base/qthelp_url.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "window/window_controller.h" #include "window/window_controller.h"
#include "core/click_handler_types.h"
#include "auth_session.h" #include "auth_session.h"
#include "storage/localimageloader.h" #include "storage/localimageloader.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
@ -46,17 +48,86 @@ Value::Type ConvertType(const MTPSecureValueType &type) {
Unexpected("Type in secureValueType 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 } // namespace
FormRequest::FormRequest( FormRequest::FormRequest(
UserId botId, UserId botId,
const QString &scope, const QString &scope,
const QString &callbackUrl, const QString &callbackUrl,
const QString &publicKey) const QString &publicKey,
const QString &payload)
: botId(botId) : botId(botId)
, scope(scope) , scope(scope)
, callbackUrl(callbackUrl) , callbackUrl(callbackUrl)
, publicKey(publicKey) { , publicKey(publicKey)
, payload(payload) {
} }
EditFile::EditFile( EditFile::EditFile(
@ -111,7 +182,7 @@ FormController::FormController(
not_null<Window::Controller*> controller, not_null<Window::Controller*> controller,
const FormRequest &request) const FormRequest &request)
: _controller(controller) : _controller(controller)
, _request(request) , _request(PreprocessRequest(request))
, _view(std::make_unique<PanelController>(this)) { , _view(std::make_unique<PanelController>(this)) {
} }
@ -136,6 +207,99 @@ bytes::vector FormController::passwordHashForAuth(
_password.salt)); _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) { void FormController::submitPassword(const QString &password) {
Expects(!_password.salt.empty()); Expects(!_password.salt.empty());
@ -231,19 +395,7 @@ bool FormController::validateValueSecrets(Value &value) {
return false; return false;
} }
} }
for (auto &file : value.files) { const auto validateFileSecret = [&](File &file) {
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;
file.secret = DecryptValueSecret( file.secret = DecryptValueSecret(
file.encryptedSecret, file.encryptedSecret,
_secret, _secret,
@ -253,6 +405,15 @@ bool FormController::validateValueSecrets(Value &value) {
"Forgetting files and data :(")); "Forgetting files and data :("));
return false; return false;
} }
return true;
};
for (auto &scan : value.scans) {
if (!validateFileSecret(scan)) {
return false;
}
}
if (value.selfie && !validateFileSecret(*value.selfie)) {
return false;
} }
return true; return true;
} }
@ -273,31 +434,31 @@ void FormController::uploadScan(
not_null<const Value*> value, not_null<const Value*> value,
QByteArray &&content) { QByteArray &&content) {
const auto nonconst = findValue(value); const auto nonconst = findValue(value);
auto fileIndex = int(nonconst->filesInEdit.size()); auto scanIndex = int(nonconst->scansInEdit.size());
nonconst->filesInEdit.emplace_back( nonconst->scansInEdit.emplace_back(
nonconst, nonconst,
File(), File(),
nullptr); nullptr);
auto &file = nonconst->filesInEdit.back(); auto &scan = nonconst->scansInEdit.back();
encryptFile(file, std::move(content), [=](UploadScanData &&result) { encryptFile(scan, std::move(content), [=](UploadScanData &&result) {
Expects(fileIndex >= 0 && fileIndex < nonconst->filesInEdit.size()); Expects(scanIndex >= 0 && scanIndex < nonconst->scansInEdit.size());
uploadEncryptedFile( uploadEncryptedFile(
nonconst->filesInEdit[fileIndex], nonconst->scansInEdit[scanIndex],
std::move(result)); std::move(result));
}); });
} }
void FormController::deleteScan( void FormController::deleteScan(
not_null<const Value*> value, not_null<const Value*> value,
int fileIndex) { int scanIndex) {
scanDeleteRestore(value, fileIndex, true); scanDeleteRestore(value, scanIndex, true);
} }
void FormController::restoreScan( void FormController::restoreScan(
not_null<const Value*> value, not_null<const Value*> value,
int fileIndex) { int scanIndex) {
scanDeleteRestore(value, fileIndex, false); scanDeleteRestore(value, scanIndex, false);
} }
void FormController::uploadSelfie( void FormController::uploadSelfie(
@ -371,14 +532,14 @@ void FormController::encryptFile(
void FormController::scanDeleteRestore( void FormController::scanDeleteRestore(
not_null<const Value*> value, not_null<const Value*> value,
int fileIndex, int scanIndex,
bool deleted) { bool deleted) {
Expects(fileIndex >= 0 && fileIndex < value->filesInEdit.size()); Expects(scanIndex >= 0 && scanIndex < value->scansInEdit.size());
const auto nonconst = findValue(value); const auto nonconst = findValue(value);
auto &file = nonconst->filesInEdit[fileIndex]; auto &scan = nonconst->scansInEdit[scanIndex];
file.deleted = deleted; scan.deleted = deleted;
_scanUpdated.fire(&file); _scanUpdated.fire(&scan);
} }
void FormController::selfieDeleteRestore( void FormController::selfieDeleteRestore(
@ -387,9 +548,9 @@ void FormController::selfieDeleteRestore(
Expects(value->selfieInEdit.has_value()); Expects(value->selfieInEdit.has_value());
const auto nonconst = findValue(value); const auto nonconst = findValue(value);
auto &file = *nonconst->selfieInEdit; auto &scan = *nonconst->selfieInEdit;
file.deleted = deleted; scan.deleted = deleted;
_scanUpdated.fire(&file); _scanUpdated.fire(&scan);
} }
void FormController::subscribeToUploader() { void FormController::subscribeToUploader() {
@ -502,6 +663,11 @@ auto FormController::valueSaveFinished() const
return _valueSaveFinished.events(); return _valueSaveFinished.events();
} }
auto FormController::valueError() const
-> rpl::producer<not_null<const Value*>> {
return _valueError.events();
}
auto FormController::verificationNeeded() const auto FormController::verificationNeeded() const
-> rpl::producer<not_null<const Value*>> { -> rpl::producer<not_null<const Value*>> {
return _verificationNeeded.events(); return _verificationNeeded.events();
@ -598,14 +764,14 @@ void FormController::startValueEdit(not_null<const Value*> value) {
if (savingValue(nonconst)) { if (savingValue(nonconst)) {
return; return;
} }
for (auto &file : nonconst->files) { for (auto &scan : nonconst->scans) {
loadFile(file); loadFile(scan);
} }
if (nonconst->selfie) { if (nonconst->selfie) {
loadFile(*nonconst->selfie); loadFile(*nonconst->selfie);
} }
nonconst->filesInEdit = ranges::view::all( nonconst->scansInEdit = ranges::view::all(
nonconst->files nonconst->scans
) | ranges::view::transform([=](const File &file) { ) | ranges::view::transform([=](const File &file) {
return EditFile(nonconst, file, nullptr); return EditFile(nonconst, file, nullptr);
}) | ranges::to_vector; }) | ranges::to_vector;
@ -729,7 +895,7 @@ void FormController::clearValueEdit(not_null<Value*> value) {
if (savingValue(value)) { if (savingValue(value)) {
return; return;
} }
value->filesInEdit.clear(); value->scansInEdit.clear();
value->selfieInEdit = base::none; value->selfieInEdit = base::none;
value->data.encryptedSecretInEdit.clear(); value->data.encryptedSecretInEdit.clear();
value->data.hashInEdit.clear(); value->data.hashInEdit.clear();
@ -770,8 +936,8 @@ bool FormController::editValueChanged(
not_null<const Value*> value, not_null<const Value*> value,
const ValueMap &data) const { const ValueMap &data) const {
auto filesCount = 0; auto filesCount = 0;
for (const auto &file : value->filesInEdit) { for (const auto &scan : value->scansInEdit) {
if (editFileChanged(file)) { if (editFileChanged(scan)) {
return true; return true;
} }
} }
@ -804,7 +970,7 @@ void FormController::saveValueEdit(
if (!editValueChanged(nonconst, data)) { if (!editValueChanged(nonconst, data)) {
nonconst->saveRequestId = -1; nonconst->saveRequestId = -1;
crl::on_main(this, [=] { crl::on_main(this, [=] {
base::take(nonconst->filesInEdit); base::take(nonconst->scansInEdit);
base::take(nonconst->selfieInEdit); base::take(nonconst->selfieInEdit);
base::take(nonconst->data.encryptedSecretInEdit); base::take(nonconst->data.encryptedSecretInEdit);
base::take(nonconst->data.hashInEdit); base::take(nonconst->data.hashInEdit);
@ -848,12 +1014,12 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
}; };
auto inputFiles = QVector<MTPInputSecureFile>(); auto inputFiles = QVector<MTPInputSecureFile>();
inputFiles.reserve(value->filesInEdit.size()); inputFiles.reserve(value->scansInEdit.size());
for (const auto &file : value->filesInEdit) { for (const auto &scan : value->scansInEdit) {
if (file.deleted) { if (scan.deleted) {
continue; continue;
} }
inputFiles.push_back(inputFile(file)); inputFiles.push_back(inputFile(scan));
} }
if (value->data.secret.empty()) { if (value->data.secret.empty()) {
@ -894,11 +1060,11 @@ void FormController::saveEncryptedValue(not_null<Value*> value) {
} }
Unexpected("Value type in saveEncryptedValue()."); Unexpected("Value type in saveEncryptedValue().");
}(); }();
const auto flags = ((value->filesInEdit.empty() const auto flags = ((value->scansInEdit.empty()
&& value->data.parsedInEdit.fields.empty()) && value->data.parsedInEdit.fields.empty())
? MTPDinputSecureValue::Flag(0) ? MTPDinputSecureValue::Flag(0)
: MTPDinputSecureValue::Flag::f_data) : MTPDinputSecureValue::Flag::f_data)
| (value->filesInEdit.empty() | (value->scansInEdit.empty()
? MTPDinputSecureValue::Flag(0) ? MTPDinputSecureValue::Flag(0)
: MTPDinputSecureValue::Flag::f_files) : MTPDinputSecureValue::Flag::f_files)
| ((value->selfieInEdit && !value->selfieInEdit->deleted) | ((value->selfieInEdit && !value->selfieInEdit->deleted)
@ -954,27 +1120,15 @@ void FormController::sendSaveRequest(
data, data,
MTP_long(_secretId) MTP_long(_secretId)
)).done([=](const MTPSecureValue &result) { )).done([=](const MTPSecureValue &result) {
Expects(result.type() == mtpc_secureValue); auto filesInEdit = base::take(value->scansInEdit);
value->saveRequestId = 0;
const auto filesInEdit = base::take(value->filesInEdit);
auto selfiesInEdit = std::vector<EditFile>();
if (auto selfie = base::take(value->selfieInEdit)) { 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(); const auto editScreens = value->editScreens;
value->files = data.has_files() *value = parseValue(result, filesInEdit);
? parseFiles(data.vfiles.v, filesInEdit) decryptValue(*value);
: std::vector<File>(); value->editScreens = editScreens;
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);
_valueSaveFinished.fire_copy(value); _valueSaveFinished.fire_copy(value);
}).fail([=](const RPCError &error) { }).fail([=](const RPCError &error) {
@ -1167,12 +1321,15 @@ void FormController::generateSecret(bytes::const_span password) {
} }
void FormController::requestForm() { void FormController::requestForm() {
auto normalizedKey = _request.publicKey; if (_request.payload.isEmpty()) {
normalizedKey.replace("\r\n", "\n"); _formRequestId = -1;
Ui::show(Box<InformBox>(lang(lng_passport_form_error)));
return;
}
_formRequestId = request(MTPaccount_GetAuthorizationForm( _formRequestId = request(MTPaccount_GetAuthorizationForm(
MTP_int(_request.botId), MTP_int(_request.botId),
MTP_string(_request.scope), MTP_string(_request.scope),
MTP_bytes(normalizedKey.toUtf8()) MTP_string(_request.publicKey)
)).done([=](const MTPaccount_AuthorizationForm &result) { )).done([=](const MTPaccount_AuthorizationForm &result) {
_formRequestId = 0; _formRequestId = 0;
formDone(result); formDone(result);
@ -1250,12 +1407,14 @@ void FormController::fillDownloadedFile(
} }
auto FormController::parseValue( auto FormController::parseValue(
const MTPSecureValue &value) const -> Value { const MTPSecureValue &value,
const std::vector<EditFile> &editData) const -> Value {
Expects(value.type() == mtpc_secureValue); Expects(value.type() == mtpc_secureValue);
const auto &data = value.c_secureValue(); const auto &data = value.c_secureValue();
const auto type = ConvertType(data.vtype); const auto type = ConvertType(data.vtype);
auto result = Value(type); auto result = Value(type);
result.submitHash = bytes::make_vector(data.vhash.v);
if (data.has_data()) { if (data.has_data()) {
Assert(data.vdata.type() == mtpc_secureData); Assert(data.vdata.type() == mtpc_secureData);
const auto &fields = data.vdata.c_secureData(); const auto &fields = data.vdata.c_secureData();
@ -1264,10 +1423,10 @@ auto FormController::parseValue(
result.data.encryptedSecret = bytes::make_vector(fields.vsecret.v); result.data.encryptedSecret = bytes::make_vector(fields.vsecret.v);
} }
if (data.has_files()) { if (data.has_files()) {
result.files = parseFiles(data.vfiles.v); result.scans = parseFiles(data.vfiles.v, editData);
} }
if (data.has_selfie()) { if (data.has_selfie()) {
result.selfie = parseFile(data.vselfie); result.selfie = parseFile(data.vselfie, editData);
} }
if (data.has_plain_data()) { if (data.has_plain_data()) {
switch (data.vplain_data.type()) { switch (data.vplain_data.type()) {
@ -1281,7 +1440,6 @@ auto FormController::parseValue(
} break; } break;
} }
} }
// #TODO passport selfie
return result; return result;
} }
@ -1290,9 +1448,9 @@ auto FormController::findEditFile(const FullMsgId &fullId) -> EditFile* {
return (file.uploadData && file.uploadData->fullId == fullId); return (file.uploadData && file.uploadData->fullId == fullId);
}; };
for (auto &[type, value] : _form.values) { for (auto &[type, value] : _form.values) {
for (auto &file : value.filesInEdit) { for (auto &scan : value.scansInEdit) {
if (found(file)) { if (found(scan)) {
return &file; return &scan;
} }
} }
if (value.selfieInEdit && found(*value.selfieInEdit)) { 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); return (file.fields.dcId == key.dcId && file.fields.id == key.id);
}; };
for (auto &[type, value] : _form.values) { for (auto &[type, value] : _form.values) {
for (auto &file : value.filesInEdit) { for (auto &scan : value.scansInEdit) {
if (found(file)) { if (found(scan)) {
return &file; return &scan;
} }
} }
if (value.selfieInEdit && found(*value.selfieInEdit)) { 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); return (file.dcId == key.dcId) && (file.id == key.id);
}; };
for (auto &[type, value] : _form.values) { for (auto &[type, value] : _form.values) {
for (auto &file : value.files) { for (auto &scan : value.scans) {
if (found(file)) { if (found(scan)) {
return { &value, &file }; return { &value, &scan };
} }
} }
if (value.selfie && found(*value.selfie)) { if (value.selfie && found(*value.selfie)) {

View file

@ -31,12 +31,14 @@ struct FormRequest {
UserId botId, UserId botId,
const QString &scope, const QString &scope,
const QString &callbackUrl, const QString &callbackUrl,
const QString &publicKey); const QString &publicKey,
const QString &payload);
UserId botId; UserId botId;
QString scope; QString scope;
QString callbackUrl; QString callbackUrl;
QString publicKey; QString publicKey;
QString payload;
}; };
@ -142,11 +144,12 @@ struct Value {
Type type; Type type;
ValueData data; ValueData data;
std::vector<File> files; std::vector<File> scans;
std::vector<EditFile> filesInEdit; std::vector<EditFile> scansInEdit;
base::optional<File> selfie; base::optional<File> selfie;
base::optional<EditFile> selfieInEdit; base::optional<EditFile> selfieInEdit;
Verification verification; Verification verification;
bytes::vector submitHash;
int editScreens = 0; int editScreens = 0;
mtpRequestId saveRequestId = 0; mtpRequestId saveRequestId = 0;
@ -204,7 +207,7 @@ public:
void show(); void show();
UserData *bot() const; UserData *bot() const;
QString privacyPolicyUrl() const; QString privacyPolicyUrl() const;
void submit();
void submitPassword(const QString &password); void submitPassword(const QString &password);
rpl::producer<QString> passwordError() const; rpl::producer<QString> passwordError() const;
QString passwordHint() const; QString passwordHint() const;
@ -223,6 +226,7 @@ public:
rpl::producer<not_null<const EditFile*>> scanUpdated() const; rpl::producer<not_null<const EditFile*>> scanUpdated() const;
rpl::producer<not_null<const Value*>> valueSaveFinished() 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*>> verificationNeeded() const;
rpl::producer<not_null<const Value*>> verificationUpdate() const; rpl::producer<not_null<const Value*>> verificationUpdate() const;
void verify(not_null<const Value*> value, const QString &code); void verify(not_null<const Value*> value, const QString &code);
@ -244,6 +248,10 @@ public:
~FormController(); ~FormController();
private: private:
struct FinalData {
QVector<MTPSecureValueHash> hashes;
QByteArray credentials;
};
EditFile *findEditFile(const FullMsgId &fullId); EditFile *findEditFile(const FullMsgId &fullId);
EditFile *findEditFile(const FileKey &key); EditFile *findEditFile(const FileKey &key);
std::pair<Value*, File*> findFile(const FileKey &key); std::pair<Value*, File*> findFile(const FileKey &key);
@ -256,13 +264,15 @@ private:
void formFail(const RPCError &error); void formFail(const RPCError &error);
void parseForm(const MTPaccount_AuthorizationForm &result); void parseForm(const MTPaccount_AuthorizationForm &result);
void showForm(); void showForm();
Value parseValue(const MTPSecureValue &value) const; Value parseValue(
const MTPSecureValue &value,
const std::vector<EditFile> &editData = {}) const;
std::vector<File> parseFiles( std::vector<File> parseFiles(
const QVector<MTPSecureFile> &data, const QVector<MTPSecureFile> &data,
const std::vector<EditFile> &editData = {}) const; const std::vector<EditFile> &editData) const;
base::optional<File> parseFile( base::optional<File> parseFile(
const MTPSecureFile &data, const MTPSecureFile &data,
const std::vector<EditFile> &editData = {}) const; const std::vector<EditFile> &editData) const;
void fillDownloadedFile( void fillDownloadedFile(
File &destination, File &destination,
const std::vector<EditFile> &source) const; const std::vector<EditFile> &source) const;
@ -330,6 +340,7 @@ private:
void sendSaveRequest( void sendSaveRequest(
not_null<Value*> value, not_null<Value*> value,
const MTPInputSecureValue &data); const MTPInputSecureValue &data);
FinalData prepareFinalData() const;
not_null<Window::Controller*> _controller; not_null<Window::Controller*> _controller;
FormRequest _request; FormRequest _request;
@ -346,6 +357,7 @@ private:
rpl::event_stream<not_null<const EditFile*>> _scanUpdated; rpl::event_stream<not_null<const EditFile*>> _scanUpdated;
rpl::event_stream<not_null<const Value*>> _valueSaveFinished; 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*>> _verificationNeeded;
rpl::event_stream<not_null<const Value*>> _verificationUpdate; rpl::event_stream<not_null<const Value*>> _verificationUpdate;
@ -355,6 +367,7 @@ private:
mtpRequestId _saveSecretRequestId = 0; mtpRequestId _saveSecretRequestId = 0;
rpl::event_stream<> _secretReady; rpl::event_stream<> _secretReady;
rpl::event_stream<QString> _passwordError; rpl::event_stream<QString> _passwordError;
mtpRequestId _submitRequestId = 0;
rpl::lifetime _uploaderSubscriptions; rpl::lifetime _uploaderSubscriptions;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -8,6 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_form_view_controller.h" #include "passport/passport_form_view_controller.h"
#include "passport/passport_form_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 Passport {
namespace { namespace {
@ -57,7 +61,8 @@ Scope::Scope(Type type, not_null<const Value*> fields)
, fields(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>(); auto scopes = std::map<Scope::Type, Scope>();
const auto &form = controller->form(); const auto &form = controller->form();
const auto findValue = [&](const Value::Type type) { 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) i->second.selfieRequired = (scopeType == Scope::Type::Identity)
&& form.identitySelfieRequired; && form.identitySelfieRequired;
const auto alreadyIt = ranges::find( const auto alreadyIt = ranges::find(
i->second.files, i->second.documents,
type, type,
[](not_null<const Value*> value) { return value->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." LOG(("API Error: Value type %1 multiple times in request."
).arg(int(type))); ).arg(int(type)));
continue; continue;
} else if (type != fieldsType) { } else if (type != fieldsType) {
i->second.files.push_back(findValue(type)); i->second.documents.push_back(findValue(type));
} }
} }
auto result = std::vector<Scope>(); auto result = std::vector<Scope>();
@ -93,4 +98,168 @@ std::vector<Scope> ComputeScopes(not_null<FormController*> controller) {
return result; 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 } // namespace Passport

View file

@ -22,11 +22,20 @@ struct Scope {
Type type; Type type;
not_null<const Value*> fields; not_null<const Value*> fields;
std::vector<not_null<const Value*>> files; std::vector<not_null<const Value*>> documents;
bool selfieRequired = false; 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 { class ViewController {
public: public:

View file

@ -18,13 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "layout.h" #include "layout.h"
namespace Passport { namespace Passport {
namespace {
PanelEditDocument::Scheme GetDocumentScheme( EditDocumentScheme GetDocumentScheme(
Scope::Type type, Scope::Type type,
base::optional<Value::Type> scansType = base::none) { base::optional<Value::Type> scansType) {
using Scheme = PanelEditDocument::Scheme; using Scheme = EditDocumentScheme;
using ValueClass = Scheme::ValueClass;
const auto DontFormat = nullptr; const auto DontFormat = nullptr;
const auto CountryFormat = [](const QString &value) { const auto CountryFormat = [](const QString &value) {
const auto result = CountrySelectBox::NameByISO(value); const auto result = CountrySelectBox::NameByISO(value);
@ -78,7 +77,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
} }
result.rows = { result.rows = {
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("first_name"), qsl("first_name"),
lang(lng_passport_first_name), lang(lng_passport_first_name),
@ -86,7 +85,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("last_name"), qsl("last_name"),
lang(lng_passport_last_name), lang(lng_passport_last_name),
@ -94,7 +93,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Date, PanelDetailsType::Date,
qsl("birth_date"), qsl("birth_date"),
lang(lng_passport_birth_date), lang(lng_passport_birth_date),
@ -102,7 +101,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Gender, PanelDetailsType::Gender,
qsl("gender"), qsl("gender"),
lang(lng_passport_gender), lang(lng_passport_gender),
@ -110,7 +109,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
GenderFormat, GenderFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Country, PanelDetailsType::Country,
qsl("country_code"), qsl("country_code"),
lang(lng_passport_country), lang(lng_passport_country),
@ -118,7 +117,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
CountryFormat, CountryFormat,
}, },
{ {
Scheme::ValueType::Scans, ValueClass::Scans,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("document_no"), qsl("document_no"),
lang(lng_passport_document_number), lang(lng_passport_document_number),
@ -126,7 +125,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Scans, ValueClass::Scans,
PanelDetailsType::Date, PanelDetailsType::Date,
qsl("expiry_date"), qsl("expiry_date"),
lang(lng_passport_expiry_date), lang(lng_passport_expiry_date),
@ -157,7 +156,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
} }
result.rows = { result.rows = {
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("street_line1"), qsl("street_line1"),
lang(lng_passport_street), lang(lng_passport_street),
@ -165,7 +164,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("street_line2"), qsl("street_line2"),
lang(lng_passport_street), lang(lng_passport_street),
@ -173,7 +172,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("city"), qsl("city"),
lang(lng_passport_city), lang(lng_passport_city),
@ -181,7 +180,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("state"), qsl("state"),
lang(lng_passport_state), lang(lng_passport_state),
@ -189,7 +188,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
DontFormat, DontFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Country, PanelDetailsType::Country,
qsl("country_code"), qsl("country_code"),
lang(lng_passport_country), lang(lng_passport_country),
@ -197,7 +196,7 @@ PanelEditDocument::Scheme GetDocumentScheme(
CountryFormat, CountryFormat,
}, },
{ {
Scheme::ValueType::Fields, ValueClass::Fields,
PanelDetailsType::Text, PanelDetailsType::Text,
qsl("post_code"), qsl("post_code"),
lang(lng_passport_postcode), lang(lng_passport_postcode),
@ -211,11 +210,13 @@ PanelEditDocument::Scheme GetDocumentScheme(
Unexpected("Type in GetDocumentScheme()."); Unexpected("Type in GetDocumentScheme().");
} }
PanelEditContact::Scheme GetContactScheme(Scope::Type type) { EditContactScheme GetContactScheme(Scope::Type type) {
using Scheme = PanelEditContact::Scheme; using Scheme = EditContactScheme;
using ValueType = Scheme::ValueType;
switch (type) { switch (type) {
case Scope::Type::Phone: { case Scope::Type::Phone: {
auto result = Scheme(Scheme::ValueType::Phone); auto result = Scheme(ValueType::Phone);
result.aboutExisting = lang(lng_passport_use_existing_phone); result.aboutExisting = lang(lng_passport_use_existing_phone);
result.newHeader = lang(lng_passport_new_phone); result.newHeader = lang(lng_passport_new_phone);
result.aboutNew = lang(lng_passport_new_phone_code); result.aboutNew = lang(lng_passport_new_phone_code);
@ -234,7 +235,7 @@ PanelEditContact::Scheme GetContactScheme(Scope::Type type) {
} break; } break;
case Scope::Type::Email: { case Scope::Type::Email: {
auto result = Scheme(Scheme::ValueType::Text); auto result = Scheme(ValueType::Text);
result.aboutExisting = lang(lng_passport_use_existing_email); result.aboutExisting = lang(lng_passport_use_existing_email);
result.newHeader = lang(lng_passport_new_email); result.newHeader = lang(lng_passport_new_email);
result.newPlaceholder = langFactory(lng_passport_email_title); result.newPlaceholder = langFactory(lng_passport_email_title);
@ -253,8 +254,6 @@ PanelEditContact::Scheme GetContactScheme(Scope::Type type) {
Unexpected("Type in GetContactScheme()."); Unexpected("Type in GetContactScheme().");
} }
} // namespace
BoxPointer::BoxPointer(QPointer<BoxContent> value) BoxPointer::BoxPointer(QPointer<BoxContent> value)
: _value(value) { : _value(value) {
} }
@ -323,158 +322,6 @@ QString PanelController::privacyPolicyUrl() const {
return _form->privacyPolicyUrl(); 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( void PanelController::fillRows(
base::lambda<void( base::lambda<void(
QString title, QString title,
@ -484,15 +331,18 @@ void PanelController::fillRows(
_scopes = ComputeScopes(_form); _scopes = ComputeScopes(_form);
} }
for (const auto &scope : _scopes) { for (const auto &scope : _scopes) {
const auto row = collectRowInfo(scope); const auto row = ComputeScopeRow(scope);
const auto ready = collectRowReadyString(scope);
callback( callback(
row.title, row.title,
ready.isEmpty() ? row.description : ready, row.ready.isEmpty() ? row.description : row.ready,
!ready.isEmpty()); !row.ready.isEmpty());
} }
} }
void PanelController::submitForm() {
_form->submit();
}
void PanelController::submitPassword(const QString &password) { void PanelController::submitPassword(const QString &password) {
_form->submitPassword(password); _form->submitPassword(password);
} }
@ -515,71 +365,71 @@ QString PanelController::defaultPhoneNumber() const {
void PanelController::uploadScan(QByteArray &&content) { void PanelController::uploadScan(QByteArray &&content) {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
_form->uploadScan( _form->uploadScan(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
std::move(content)); std::move(content));
} }
void PanelController::deleteScan(int fileIndex) { void PanelController::deleteScan(int fileIndex) {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
_form->deleteScan( _form->deleteScan(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
fileIndex); fileIndex);
} }
void PanelController::restoreScan(int fileIndex) { void PanelController::restoreScan(int fileIndex) {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
_form->restoreScan( _form->restoreScan(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
fileIndex); fileIndex);
} }
void PanelController::uploadSelfie(QByteArray &&content) { void PanelController::uploadSelfie(QByteArray &&content) {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
Expects(_editScope->selfieRequired); Expects(_editScope->selfieRequired);
_form->uploadSelfie( _form->uploadSelfie(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
std::move(content)); std::move(content));
} }
void PanelController::deleteSelfie() { void PanelController::deleteSelfie() {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
Expects(_editScope->selfieRequired); Expects(_editScope->selfieRequired);
_form->deleteSelfie( _form->deleteSelfie(
_editScope->files[_editScopeFilesIndex]); _editScope->documents[_editDocumentIndex]);
} }
void PanelController::restoreSelfie() { void PanelController::restoreSelfie() {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0 Expects(_editDocumentIndex >= 0
&& _editScopeFilesIndex < _editScope->files.size()); && _editDocumentIndex < _editScope->documents.size());
Expects(_editScope->selfieRequired); Expects(_editScope->selfieRequired);
_form->restoreSelfie( _form->restoreSelfie(
_editScope->files[_editScopeFilesIndex]); _editScope->documents[_editDocumentIndex]);
} }
rpl::producer<ScanInfo> PanelController::scanUpdated() const { rpl::producer<ScanInfo> PanelController::scanUpdated() const {
return _form->scanUpdated( return _form->scanUpdated(
) | rpl::filter([=](not_null<const EditFile*> file) { ) | rpl::filter([=](not_null<const EditFile*> file) {
return (_editScope != nullptr) return (_editScope != nullptr)
&& (_editScopeFilesIndex >= 0) && (_editDocumentIndex >= 0)
&& (file->value == _editScope->files[_editScopeFilesIndex]); && (file->value == _editScope->documents[_editDocumentIndex]);
}) | rpl::map([=](not_null<const EditFile*> file) { }) | rpl::map([=](not_null<const EditFile*> file) {
return collectScanInfo(*file); return collectScanInfo(*file);
}); });
@ -587,7 +437,7 @@ rpl::producer<ScanInfo> PanelController::scanUpdated() const {
ScanInfo PanelController::collectScanInfo(const EditFile &file) const { ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
Expects(_editScope != nullptr); Expects(_editScope != nullptr);
Expects(_editScopeFilesIndex >= 0); Expects(_editDocumentIndex >= 0);
const auto status = [&] { const auto status = [&] {
if (file.fields.accessHash) { if (file.fields.accessHash) {
@ -618,10 +468,10 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
return formatDownloadText(0, file.fields.size); return formatDownloadText(0, file.fields.size);
} }
}(); }();
const auto &files = _editScope->files; const auto &documents = _editScope->documents;
auto isSelfie = (file.value == files[_editScopeFilesIndex]) auto isSelfie = (file.value == documents[_editDocumentIndex])
&& (files[_editScopeFilesIndex]->selfieInEdit.has_value()) && (documents[_editDocumentIndex]->selfieInEdit.has_value())
&& (&file == &*files[_editScopeFilesIndex]->selfieInEdit); && (&file == &*documents[_editDocumentIndex]->selfieInEdit);
return { return {
FileKey{ file.fields.id, file.fields.dcId }, FileKey{ file.fields.id, file.fields.dcId },
status, status,
@ -664,7 +514,7 @@ void PanelController::ensurePanelCreated() {
int PanelController::findNonEmptyIndex( int PanelController::findNonEmptyIndex(
const std::vector<not_null<const Value*>> &files) const { const std::vector<not_null<const Value*>> &files) const {
const auto i = ranges::find_if(files, [](not_null<const Value*> file) { const auto i = ranges::find_if(files, [](not_null<const Value*> file) {
return !file->files.empty(); return !file->scans.empty();
}); });
if (i != end(files)) { if (i != end(files)) {
return (i - begin(files)); return (i - begin(files));
@ -677,13 +527,14 @@ void PanelController::editScope(int index) {
Expects(_panel != nullptr); Expects(_panel != nullptr);
Expects(index >= 0 && index < _scopes.size()); Expects(index >= 0 && index < _scopes.size());
if (_scopes[index].files.empty()) { if (_scopes[index].documents.empty()) {
editScope(index, -1); editScope(index, -1);
} else { } else {
const auto filesIndex = findNonEmptyIndex(_scopes[index].files); const auto documentIndex = findNonEmptyIndex(
if (filesIndex >= 0) { _scopes[index].documents);
editScope(index, filesIndex); if (documentIndex >= 0) {
} else if (_scopes[index].files.size() > 1) { editScope(index, documentIndex);
} else if (_scopes[index].documents.size() > 1) {
requestScopeFilesType(index); requestScopeFilesType(index);
} else { } else {
editWithUpload(index, 0); editWithUpload(index, 0);
@ -696,14 +547,14 @@ void PanelController::requestScopeFilesType(int index) {
Expects(index >= 0 && index < _scopes.size()); Expects(index >= 0 && index < _scopes.size());
const auto type = _scopes[index].type; const auto type = _scopes[index].type;
_scopeFilesTypeBox = [&] { _scopeDocumentTypeBox = [&] {
if (type == Scope::Type::Identity) { if (type == Scope::Type::Identity) {
return show(RequestIdentityType( return show(RequestIdentityType(
[=](int filesIndex) { [=](int documentIndex) {
editWithUpload(index, filesIndex); editWithUpload(index, documentIndex);
}, },
ranges::view::all( ranges::view::all(
_scopes[index].files _scopes[index].documents
) | ranges::view::transform([](auto value) { ) | ranges::view::transform([](auto value) {
return value->type; return value->type;
}) | ranges::view::transform([](Value::Type type) { }) | ranges::view::transform([](Value::Type type) {
@ -720,11 +571,11 @@ void PanelController::requestScopeFilesType(int index) {
}) | ranges::to_vector)); }) | ranges::to_vector));
} else if (type == Scope::Type::Address) { } else if (type == Scope::Type::Address) {
return show(RequestAddressType( return show(RequestAddressType(
[=](int filesIndex) { [=](int documentIndex) {
editWithUpload(index, filesIndex); editWithUpload(index, documentIndex);
}, },
ranges::view::all( ranges::view::all(
_scopes[index].files _scopes[index].documents
) | ranges::view::transform([](auto value) { ) | ranges::view::transform([](auto value) {
return value->type; return value->type;
}) | ranges::view::transform([](Value::Type 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(_panel != nullptr);
Expects(index >= 0 && index < _scopes.size()); Expects(index >= 0 && index < _scopes.size());
Expects(filesIndex >= 0 && filesIndex < _scopes[index].files.size()); Expects(documentIndex >= 0
&& documentIndex < _scopes[index].documents.size());
EditScans::ChooseScan( EditScans::ChooseScan(
base::lambda_guarded(_panel.get(), base::lambda_guarded(_panel.get(),
[=](QByteArray &&content) { [=](QByteArray &&content) {
base::take(_scopeFilesTypeBox); base::take(_scopeDocumentTypeBox);
editScope(index, filesIndex); editScope(index, documentIndex);
uploadScan(std::move(content)); uploadScan(std::move(content));
})); }));
} }
void PanelController::editScope(int index, int filesIndex) { void PanelController::editScope(int index, int documentIndex) {
Expects(_panel != nullptr); Expects(_panel != nullptr);
Expects(index >= 0 && index < _scopes.size()); Expects(index >= 0 && index < _scopes.size());
Expects((filesIndex < 0) Expects((documentIndex < 0)
|| (filesIndex >= 0 && filesIndex < _scopes[index].files.size())); || (documentIndex >= 0
&& documentIndex < _scopes[index].documents.size()));
_editScope = &_scopes[index]; _editScope = &_scopes[index];
_editScopeFilesIndex = filesIndex; _editDocumentIndex = documentIndex;
_form->startValueEdit(_editScope->fields); _form->startValueEdit(_editScope->fields);
if (_editScopeFilesIndex >= 0) { if (_editDocumentIndex >= 0) {
_form->startValueEdit(_editScope->files[_editScopeFilesIndex]); _form->startValueEdit(_editScope->documents[_editDocumentIndex]);
} }
auto content = [&]() -> object_ptr<Ui::RpWidget> { auto content = [&]() -> object_ptr<Ui::RpWidget> {
switch (_editScope->type) { switch (_editScope->type) {
case Scope::Type::Identity: case Scope::Type::Identity:
case Scope::Type::Address: { case Scope::Type::Address: {
const auto &files = _editScope->files; const auto &documents = _editScope->documents;
auto result = (_editScopeFilesIndex >= 0) auto result = (_editDocumentIndex >= 0)
? object_ptr<PanelEditDocument>( ? object_ptr<PanelEditDocument>(
_panel.get(), _panel.get(),
this, this,
GetDocumentScheme( GetDocumentScheme(
_editScope->type, _editScope->type,
files[_editScopeFilesIndex]->type), documents[_editDocumentIndex]->type),
_editScope->fields->data.parsedInEdit, _editScope->fields->data.parsedInEdit,
files[_editScopeFilesIndex]->data.parsedInEdit, documents[_editDocumentIndex]->data.parsedInEdit,
valueFiles(*files[_editScopeFilesIndex]), valueFiles(*documents[_editDocumentIndex]),
(_editScope->selfieRequired (_editScope->selfieRequired
? valueSelfie(*files[_editScopeFilesIndex]) ? valueSelfie(*documents[_editDocumentIndex])
: nullptr)) : nullptr))
: object_ptr<PanelEditDocument>( : object_ptr<PanelEditDocument>(
_panel.get(), _panel.get(),
@ -850,8 +703,8 @@ void PanelController::processValueSaveFinished(
} }
const auto value1 = _editScope->fields; const auto value1 = _editScope->fields;
const auto value2 = (_editScopeFilesIndex >= 0) const auto value2 = (_editDocumentIndex >= 0)
? _editScope->files[_editScopeFilesIndex].get() ? _editScope->documents[_editDocumentIndex].get()
: nullptr; : nullptr;
if (value == value1 || value == value2) { if (value == value1 || value == value2) {
if (!_form->savingValue(value1) if (!_form->savingValue(value1)
@ -925,8 +778,8 @@ void PanelController::processVerificationNeeded(
std::vector<ScanInfo> PanelController::valueFiles( std::vector<ScanInfo> PanelController::valueFiles(
const Value &value) const { const Value &value) const {
auto result = std::vector<ScanInfo>(); auto result = std::vector<ScanInfo>();
for (const auto &file : value.filesInEdit) { for (const auto &scan : value.scansInEdit) {
result.push_back(collectScanInfo(file)); result.push_back(collectScanInfo(scan));
} }
return result; return result;
} }
@ -943,9 +796,9 @@ std::unique_ptr<ScanInfo> PanelController::valueSelfie(
void PanelController::cancelValueEdit() { void PanelController::cancelValueEdit() {
if (const auto scope = base::take(_editScope)) { if (const auto scope = base::take(_editScope)) {
_form->cancelValueEdit(scope->fields); _form->cancelValueEdit(scope->fields);
const auto index = std::exchange(_editScopeFilesIndex, -1); const auto index = std::exchange(_editDocumentIndex, -1);
if (index >= 0) { 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); Expects(_editScope != nullptr);
_form->saveValueEdit(_editScope->fields, std::move(data)); _form->saveValueEdit(_editScope->fields, std::move(data));
if (_editScopeFilesIndex >= 0) { if (_editDocumentIndex >= 0) {
_form->saveValueEdit( _form->saveValueEdit(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
std::move(filesData)); std::move(filesData));
} else { } else {
Assert(filesData.fields.empty()); Assert(filesData.fields.empty());
@ -971,9 +824,9 @@ bool PanelController::editScopeChanged(
if (_form->editValueChanged(_editScope->fields, data)) { if (_form->editValueChanged(_editScope->fields, data)) {
return true; return true;
} else if (_editScopeFilesIndex >= 0) { } else if (_editDocumentIndex >= 0) {
return _form->editValueChanged( return _form->editValueChanged(
_editScope->files[_editScopeFilesIndex], _editScope->documents[_editDocumentIndex],
filesData); filesData);
} }
return false; return false;

View file

@ -15,6 +15,14 @@ namespace Passport {
class FormController; class FormController;
class Panel; 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 { struct ScanInfo {
FileKey key; FileKey key;
QString status; QString status;
@ -47,7 +55,7 @@ public:
not_null<UserData*> bot() const; not_null<UserData*> bot() const;
QString privacyPolicyUrl() const; QString privacyPolicyUrl() const;
void submitForm();
void submitPassword(const QString &password); void submitPassword(const QString &password);
rpl::producer<QString> passwordError() const; rpl::producer<QString> passwordError() const;
QString passwordHint() const; QString passwordHint() const;
@ -87,14 +95,10 @@ public:
rpl::lifetime &lifetime(); rpl::lifetime &lifetime();
private: private:
struct Row {
QString title;
QString description;
};
void ensurePanelCreated(); void ensurePanelCreated();
void editScope(int index, int filesIndex); void editScope(int index, int documentIndex);
void editWithUpload(int index, int filesIndex); void editWithUpload(int index, int documentIndex);
int findNonEmptyIndex( int findNonEmptyIndex(
const std::vector<not_null<const Value*>> &files) const; const std::vector<not_null<const Value*>> &files) const;
void requestScopeFilesType(int index); void requestScopeFilesType(int index);
@ -104,8 +108,6 @@ private:
void processValueSaveFinished(not_null<const Value*> value); void processValueSaveFinished(not_null<const Value*> value);
void processVerificationNeeded(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; ScanInfo collectScanInfo(const EditFile &file) const;
QString getDefaultContactValue(Scope::Type type) const; QString getDefaultContactValue(Scope::Type type) const;
@ -116,8 +118,8 @@ private:
base::lambda<bool()> _panelHasUnsavedChanges; base::lambda<bool()> _panelHasUnsavedChanges;
BoxPointer _confirmForgetChangesBox; BoxPointer _confirmForgetChangesBox;
Scope *_editScope = nullptr; Scope *_editScope = nullptr;
int _editScopeFilesIndex = -1; int _editDocumentIndex = -1;
BoxPointer _scopeFilesTypeBox; BoxPointer _scopeDocumentTypeBox;
std::map<not_null<const Value*>, BoxPointer> _verificationBoxes; std::map<not_null<const Value*>, BoxPointer> _verificationBoxes;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -159,7 +159,7 @@ void VerifyBox::prepare() {
} // namespace } // namespace
PanelEditContact::Scheme::Scheme(ValueType type) : type(type) { EditContactScheme::EditContactScheme(ValueType type) : type(type) {
} }
PanelEditContact::PanelEditContact( PanelEditContact::PanelEditContact(

View file

@ -20,14 +20,12 @@ namespace Passport {
class PanelController; class PanelController;
class PanelEditContact : public Ui::RpWidget { struct EditContactScheme {
public:
struct Scheme {
enum class ValueType { enum class ValueType {
Phone, Phone,
Text, Text,
}; };
explicit Scheme(ValueType type); explicit EditContactScheme(ValueType type);
ValueType type; ValueType type;
@ -41,6 +39,10 @@ public:
}; };
class PanelEditContact : public Ui::RpWidget {
public:
using Scheme = EditContactScheme;
PanelEditContact( PanelEditContact(
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,

View file

@ -234,7 +234,7 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) { for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
const auto &row = _scheme.rows[i]; const auto &row = _scheme.rows[i];
auto fields = (row.type == Scheme::ValueType::Fields) auto fields = (row.valueClass == Scheme::ValueClass::Fields)
? &data ? &data
: scanData; : scanData;
if (!fields) { if (!fields) {
@ -289,7 +289,7 @@ PanelEditDocument::Result PanelEditDocument::collect() const {
auto result = Result(); auto result = Result();
for (const auto [i, field] : _details) { for (const auto [i, field] : _details) {
const auto &row = _scheme.rows[i]; const auto &row = _scheme.rows[i];
auto &fields = (row.type == Scheme::ValueType::Fields) auto &fields = (row.valueClass == Scheme::ValueClass::Fields)
? result.data ? result.data
: result.filesData; : result.filesData;
fields.fields[row.key] = field->valueCurrent(); fields.fields[row.key] = field->valueCurrent();

View file

@ -26,15 +26,13 @@ class EditScans;
class PanelDetailsRow; class PanelDetailsRow;
enum class PanelDetailsType; enum class PanelDetailsType;
class PanelEditDocument : public Ui::RpWidget { struct EditDocumentScheme {
public: enum class ValueClass {
struct Scheme {
enum class ValueType {
Fields, Fields,
Scans, Scans,
}; };
struct Row { struct Row {
ValueType type = ValueType::Fields; ValueClass valueClass = ValueClass::Fields;
PanelDetailsType inputType = PanelDetailsType(); PanelDetailsType inputType = PanelDetailsType();
QString key; QString key;
QString label; QString label;
@ -47,6 +45,10 @@ public:
}; };
class PanelEditDocument : public Ui::RpWidget {
public:
using Scheme = EditDocumentScheme;
PanelEditDocument( PanelEditDocument(
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,

View file

@ -145,6 +145,10 @@ PanelForm::PanelForm(
void PanelForm::setupControls() { void PanelForm::setupControls() {
const auto inner = setupContent(); const auto inner = setupContent();
_submit->addClickHandler([=] {
_controller->submitForm();
});
using namespace rpl::mappers; using namespace rpl::mappers;
_topShadow->toggleOn( _topShadow->toggleOn(