tdesktop/Telegram/SourceFiles/passport/passport_form_view_controll...

566 lines
16 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
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 {
std::map<Value::Type, Scope::Type> ScopeTypesMap() {
return {
{ Value::Type::PersonalDetails, Scope::Type::PersonalDetails },
{ Value::Type::Passport, Scope::Type::Identity },
{ Value::Type::DriverLicense, Scope::Type::Identity },
{ Value::Type::IdentityCard, Scope::Type::Identity },
{ Value::Type::InternalPassport, Scope::Type::Identity },
{ Value::Type::Address, Scope::Type::AddressDetails },
{ Value::Type::UtilityBill, Scope::Type::Address },
{ Value::Type::BankStatement, Scope::Type::Address },
{ Value::Type::RentalAgreement, Scope::Type::Address },
{ Value::Type::PassportRegistration, Scope::Type::Address },
{ Value::Type::TemporaryRegistration, Scope::Type::Address },
{ Value::Type::Phone, Scope::Type::Phone },
{ Value::Type::Email, Scope::Type::Email },
};
}
Scope::Type ScopeTypeForValueType(Value::Type type) {
static const auto map = ScopeTypesMap();
const auto i = map.find(type);
Assert(i != map.end());
return i->second;
}
std::map<Scope::Type, Value::Type> ScopeDetailsMap() {
return {
{ Scope::Type::PersonalDetails, Value::Type::PersonalDetails },
{ Scope::Type::Identity, Value::Type::PersonalDetails },
{ Scope::Type::AddressDetails, Value::Type::Address },
{ Scope::Type::Address, Value::Type::Address },
{ Scope::Type::Phone, Value::Type::Phone },
{ Scope::Type::Email, Value::Type::Email },
};
}
Value::Type DetailsTypeForScopeType(Scope::Type type) {
static const auto map = ScopeDetailsMap();
const auto i = map.find(type);
Assert(i != map.end());
return i->second;
}
bool InlineDetails(
const Form::Request &request,
Scope::Type into,
Value::Type details) {
const auto count = ranges::count_if(
request,
[&](const std::vector<Value::Type> &types) {
Expects(!types.empty());
return ScopeTypeForValueType(types[0]) == into;
});
if (count != 1) {
return false;
}
return ranges::any_of(
request,
[&](const std::vector<Value::Type> &types) {
Expects(!types.empty());
return (types[0] == details);
}
);
}
bool InlineDetails(const Form::Request &request, Value::Type details) {
if (details == Value::Type::PersonalDetails) {
return InlineDetails(request, Scope::Type::Identity, details);
} else if (details == Value::Type::Address) {
return InlineDetails(request, Scope::Type::Address, details);
}
return false;
}
} // namespace
Scope::Scope(Type type) : type(type) {
}
bool CanRequireSelfie(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address)
|| (scope == Scope::Type::Identity);
}
bool CanRequireScans(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address);
}
bool CanRequireTranslation(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address)
|| (scope == Scope::Type::Identity);
}
bool CanRequireNativeNames(Value::Type type) {
return (type == Value::Type::PersonalDetails);
}
bool CanHaveErrors(Value::Type type) {
return (type != Value::Type::Phone) && (type != Value::Type::Email);
}
bool ValidateForm(const Form &form) {
base::flat_set<Value::Type> values;
for (const auto &requested : form.request) {
if (requested.empty()) {
LOG(("API Error: Empty types list in authorization form row."));
return false;
}
const auto scopeType = ScopeTypeForValueType(requested[0]);
const auto ownsDetails = (scopeType != Scope::Type::Identity
&& scopeType != Scope::Type::Address);
if (ownsDetails && requested.size() != 1) {
LOG(("API Error: Large types list in authorization form row."));
return false;
}
for (const auto type : requested) {
if (values.contains(type)) {
LOG(("API Error: Value twice in authorization form row."));
return false;
}
values.emplace(type);
}
}
// Invalid errors should be skipped while parsing the form.
for (const auto &[type, value] : form.values) {
if (value.selfieRequired && !CanRequireSelfie(type)) {
LOG(("API Error: Bad value requiring selfie."));
return false;
} else if (value.translationRequired
&& !CanRequireTranslation(type)) {
LOG(("API Error: Bad value requiring translation."));
return false;
} else if (value.nativeNames && !CanRequireNativeNames(type)) {
LOG(("API Error: Bad value requiring native names."));
return false;
}
if (!value.requiresScan(FileType::Scan)) {
for (const auto &scan : value.files(FileType::Scan)) {
Assert(scan.error.isEmpty());
}
Assert(value.fileMissingError(FileType::Scan).isEmpty());
}
if (!value.requiresScan(FileType::Translation)) {
for (const auto &scan : value.files(FileType::Translation)) {
Assert(scan.error.isEmpty());
}
Assert(value.fileMissingError(FileType::Translation).isEmpty());
}
for (const auto &[type, specialScan] : value.specialScans) {
if (!value.requiresSpecialScan(type)) {
Assert(specialScan.error.isEmpty());
}
}
}
return true;
}
std::vector<Scope> ComputeScopes(const Form &form) {
auto result = std::vector<Scope>();
const auto findValue = [&](const Value::Type type) {
const auto i = form.values.find(type);
Assert(i != form.values.end());
return &i->second;
};
for (const auto &requested : form.request) {
Assert(!requested.empty());
const auto scopeType = ScopeTypeForValueType(requested[0]);
const auto detailsType = DetailsTypeForScopeType(scopeType);
const auto ownsDetails = (scopeType != Scope::Type::Identity
&& scopeType != Scope::Type::Address);
const auto inlineDetails = InlineDetails(form.request, detailsType);
if (ownsDetails && inlineDetails) {
continue;
}
result.push_back(Scope(scopeType));
auto &scope = result.back();
scope.details = (ownsDetails || inlineDetails)
? findValue(detailsType)
: nullptr;
if (ownsDetails) {
Assert(requested.size() == 1);
} else {
for (const auto type : requested) {
scope.documents.push_back(findValue(type));
}
}
}
return result;
}
QString JoinScopeRowReadyString(
std::vector<std::pair<QString, QString>> &&values) {
using Pair = std::pair<QString, QString>;
if (values.empty()) {
return QString();
}
auto result = QString();
auto size = ranges::accumulate(
values,
0,
ranges::plus(),
[](const Pair &v) { return v.second.size(); });
result.reserve(size + (values.size() - 1) * 2);
for (const auto &pair : values) {
if (pair.second.isEmpty()) {
continue;
}
if (!result.isEmpty()) {
result.append(", ");
}
result.append(pair.second);
}
return result;
}
ScopeRow DocumentRowByType(Value::Type type) {
using Type = Value::Type;
switch (type) {
case Type::Passport:
return {
tr::lng_passport_identity_passport(tr::now),
tr::lng_passport_identity_passport_upload(tr::now),
};
case Type::DriverLicense:
return {
tr::lng_passport_identity_license(tr::now),
tr::lng_passport_identity_license_upload(tr::now),
};
case Type::IdentityCard:
return {
tr::lng_passport_identity_card(tr::now),
tr::lng_passport_identity_card_upload(tr::now),
};
case Type::InternalPassport:
return {
tr::lng_passport_identity_internal(tr::now),
tr::lng_passport_identity_internal_upload(tr::now),
};
case Type::BankStatement:
return {
tr::lng_passport_address_statement(tr::now),
tr::lng_passport_address_statement_upload(tr::now),
};
case Type::UtilityBill:
return {
tr::lng_passport_address_bill(tr::now),
tr::lng_passport_address_bill_upload(tr::now),
};
case Type::RentalAgreement:
return {
tr::lng_passport_address_agreement(tr::now),
tr::lng_passport_address_agreement_upload(tr::now),
};
case Type::PassportRegistration:
return {
tr::lng_passport_address_registration(tr::now),
tr::lng_passport_address_registration_upload(tr::now),
};
case Type::TemporaryRegistration:
return {
tr::lng_passport_address_temporary(tr::now),
tr::lng_passport_address_temporary_upload(tr::now),
};
default: Unexpected("Value type in DocumentRowByType.");
}
}
QString DocumentName(Value::Type type) {
return DocumentRowByType(type).title;
}
ScopeRow DocumentsOneOfRow(
const Scope &scope,
const QString &severalTitle,
const QString &severalDescription) {
Expects(!scope.documents.empty());
const auto &documents = scope.documents;
if (documents.size() == 1) {
const auto type = documents.front()->type;
return DocumentRowByType(type);
} else if (documents.size() == 2) {
const auto type1 = documents.front()->type;
const auto type2 = documents.back()->type;
return {
tr::lng_passport_or_title(
tr::now,
lt_document,
DocumentName(type1),
lt_second_document,
DocumentName(type2)),
severalDescription,
};
}
return {
severalTitle,
severalDescription,
};
}
QString ComputeScopeRowReadyString(const Scope &scope) {
switch (scope.type) {
case Scope::Type::PersonalDetails:
case Scope::Type::Identity:
case Scope::Type::AddressDetails:
case Scope::Type::Address: {
auto list = std::vector<std::pair<QString, QString>>();
const auto pushListValue = [&](
const QString &key,
const QString &value,
const QString &keyForAttachmentTo = QString()) {
if (keyForAttachmentTo.isEmpty()) {
list.push_back({ key, value.trimmed() });
} else {
const auto i = ranges::find(
list,
keyForAttachmentTo,
[](const std::pair<QString, QString> &value) {
return value.first;
});
Assert(i != end(list));
if (const auto data = value.trimmed(); !data.isEmpty()) {
if (i->second.isEmpty()) {
i->second = data;
} else {
i->second += ' ' + data;
}
}
}
};
const auto fields = scope.details
? &scope.details->data.parsed.fields
: nullptr;
const auto document = [&]() -> const Value* {
for (const auto &document : scope.documents) {
if (document->scansAreFilled()) {
return document;
}
}
return nullptr;
}();
if (!scope.documents.empty() && !document) {
return QString();
}
if ((document && scope.documents.size() > 1)
|| (!scope.details
&& (ScopeTypeForValueType(document->type)
== Scope::Type::Address))) {
pushListValue("_type", DocumentName(document->type));
}
const auto scheme = GetDocumentScheme(
scope.type,
document ? base::make_optional(document->type) : std::nullopt,
scope.details ? scope.details->nativeNames : false,
nullptr);
using ValueClass = EditDocumentScheme::ValueClass;
const auto skipAdditional = [&] {
if (!fields) {
return false;
}
for (const auto &row : scheme.rows) {
if (row.valueClass == ValueClass::Additional) {
const auto i = fields->find(row.key);
const auto native = (i == end(*fields))
? QString()
: i->second.text;
const auto j = fields->find(row.additionalFallbackKey);
const auto latin = (j == end(*fields))
? QString()
: j->second.text;
if (latin != native) {
return false;
}
}
}
return true;
}();
for (const auto &row : scheme.rows) {
const auto format = row.format;
if (row.valueClass != ValueClass::Scans) {
if (!fields) {
continue;
} else if (row.valueClass == ValueClass::Additional
&& skipAdditional) {
continue;
}
const auto i = fields->find(row.key);
const auto text = (i == end(*fields))
? QString()
: i->second.text;
if (row.error && row.error(text).has_value()) {
return QString();
}
pushListValue(
row.key,
format ? format(text) : text,
row.keyForAttachmentTo);
} else if (scope.documents.empty()) {
continue;
} else {
const auto i = document->data.parsed.fields.find(row.key);
const auto text = (i == end(document->data.parsed.fields))
? QString()
: i->second.text;
if (row.error && row.error(text).has_value()) {
return QString();
}
pushListValue(row.key, text, row.keyForAttachmentTo);
}
}
return JoinScopeRowReadyString(std::move(list));
} break;
case Scope::Type::Phone:
case Scope::Type::Email: {
Assert(scope.details != nullptr);
const auto format = GetContactScheme(scope.type).format;
const auto &fields = scope.details->data.parsed.fields;
const auto i = fields.find("value");
return (i != end(fields))
? (format ? format(i->second.text) : i->second.text)
: QString();
} break;
}
Unexpected("Scope type in ComputeScopeRowReadyString.");
}
ScopeRow ComputeScopeRow(const Scope &scope) {
const auto addReadyError = [&](
ScopeRow &&row,
QString titleFallback = QString()) {
row.ready = ComputeScopeRowReadyString(scope);
auto errors = QStringList();
const auto addValueErrors = [&](not_null<const Value*> value) {
if (!value->error.isEmpty()) {
errors.push_back(value->error);
}
const auto addTypeErrors = [&](FileType type) {
if (!value->fileMissingError(type).isEmpty()) {
errors.push_back(value->fileMissingError(type));
}
for (const auto &scan : value->files(type)) {
if (!scan.error.isEmpty()) {
errors.push_back(scan.error);
}
}
};
addTypeErrors(FileType::Scan);
addTypeErrors(FileType::Translation);
for (const auto &[type, scan] : value->specialScans) {
if (!scan.error.isEmpty()) {
errors.push_back(scan.error);
}
}
for (const auto &[key, value] : value->data.parsed.fields) {
if (!value.error.isEmpty()) {
errors.push_back(value.error);
}
}
};
const auto document = [&]() -> const Value* {
for (const auto &document : scope.documents) {
if (document->scansAreFilled()) {
return document;
}
}
return nullptr;
}();
if (document) {
addValueErrors(document);
}
if (scope.details) {
addValueErrors(scope.details);
}
if (!errors.isEmpty()) {
row.error = errors[0];// errors.join('\n');
} else if (row.title == row.ready && !titleFallback.isEmpty()) {
row.title = titleFallback;
}
if (row.error.isEmpty()
&& row.ready.isEmpty()
&& !scope.documents.empty()) {
if (document) {
row.description = (scope.type == Scope::Type::Identity)
? tr::lng_passport_personal_details_enter(tr::now)
: tr::lng_passport_address_enter(tr::now);
} else {
const auto best = ranges::min(
scope.documents,
std::less<>(),
[](not_null<const Value*> document) {
return document->whatNotFilled();
});
const auto notFilled = best->whatNotFilled();
if (notFilled & Value::kNoTranslationFilled) {
row.description = tr::lng_passport_translation_needed(tr::now);
} else if (notFilled & Value::kNoSelfieFilled) {
row.description = tr::lng_passport_identity_selfie(tr::now);
}
}
}
return std::move(row);
};
switch (scope.type) {
case Scope::Type::PersonalDetails:
return addReadyError({
tr::lng_passport_personal_details(tr::now),
tr::lng_passport_personal_details_enter(tr::now),
});
case Scope::Type::Identity:
return addReadyError(DocumentsOneOfRow(
scope,
tr::lng_passport_identity_title(tr::now),
tr::lng_passport_identity_description(tr::now)));
case Scope::Type::AddressDetails:
return addReadyError({
tr::lng_passport_address(tr::now),
tr::lng_passport_address_enter(tr::now),
});
case Scope::Type::Address:
return addReadyError(DocumentsOneOfRow(
scope,
tr::lng_passport_address_title(tr::now),
tr::lng_passport_address_description(tr::now)));
case Scope::Type::Phone:
return addReadyError({
tr::lng_passport_phone_title(tr::now),
tr::lng_passport_phone_description(tr::now),
});
case Scope::Type::Email:
return addReadyError({
tr::lng_passport_email_title(tr::now),
tr::lng_passport_email_description(tr::now),
});
default: Unexpected("Scope type in ComputeScopeRow.");
}
}
} // namespace Passport