Display scope errors in passport.

This commit is contained in:
John Preston 2018-04-17 21:54:52 +04:00
parent 704e3c9423
commit 22bdf15825
15 changed files with 274 additions and 38 deletions

View File

@ -844,6 +844,7 @@ bool Messenger::openLocalUrl(const QString &url) {
const auto callback = params.value("callback_url", QString());
const auto publicKey = params.value("public_key", QString());
const auto payload = params.value("payload", QString());
const auto errors = params.value("errors", QString());
if (const auto window = App::wnd()) {
if (const auto controller = window->controller()) {
controller->showPassportForm(Passport::FormRequest(
@ -851,7 +852,8 @@ bool Messenger::openLocalUrl(const QString &url) {
scope,
callback,
publicKey,
payload));
payload,
errors));
return true;
}
}

View File

@ -172,6 +172,7 @@ passportUploadButton: InfoProfileButton {
}
passportUploadButtonPadding: margins(0px, 10px, 0px, 10px);
passportUploadHeaderPadding: margins(22px, 14px, 22px, 3px);
passportUploadErrorPadding: margins(22px, 5px, 22px, 5px);
passportDeleteButton: InfoProfileButton(passportUploadButton) {
textFg: attentionButtonFg;
textFgOver: attentionButtonFgOver;

View File

@ -238,6 +238,78 @@ std::map<QString, QString> DeserializeData(bytes::const_span bytes) {
return result;
}
std::vector<DataError> DeserializeErrors(bytes::const_span json) {
const auto serialized = QByteArray::fromRawData(
reinterpret_cast<const char*>(json.data()),
json.size());
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(serialized, &error);
if (error.error != QJsonParseError::NoError) {
LOG(("API Error: Could not deserialize errors JSON, error %1"
).arg(error.errorString()));
return {};
} else if (!document.isArray()) {
LOG(("API Error: Errors JSON root is not an array."));
return {};
}
auto array = document.array();
auto result = std::vector<DataError>();
for (const auto &error : array) {
if (!error.isObject()) {
LOG(("API Error: Not an object inside errors JSON."));
continue;
}
auto fields = error.toObject();
const auto typeIt = fields.constFind("type");
if (typeIt == fields.constEnd()) {
LOG(("API Error: type was not found in an error."));
continue;
} else if (!typeIt->isString()) {
LOG(("API Error: type was not a string in an error."));
continue;
}
const auto descriptionIt = fields.constFind("description");
if (descriptionIt == fields.constEnd()) {
LOG(("API Error: description was not found in an error."));
continue;
} else if (!typeIt->isString()) {
LOG(("API Error: description was not a string in an error."));
continue;
}
const auto targetIt = fields.constFind("target");
if (targetIt == fields.constEnd()) {
LOG(("API Error: target aws not found in an error."));
continue;
} else if (!targetIt->isString()) {
LOG(("API Error: target was not as string in an error."));
continue;
}
auto next = DataError();
next.type = typeIt->toString();
next.text = descriptionIt->toString();
const auto fieldIt = fields.constFind("field");
const auto fileHashIt = fields.constFind("file_hash");
if (fieldIt != fields.constEnd()) {
if (!fieldIt->isString()) {
LOG(("API Error: field was not a string in an error."));
continue;
}
next.key = fieldIt->toString();
} else if (fileHashIt != fields.constEnd()) {
if (!fileHashIt->isString()) {
LOG(("API Error: file_hash was not a string in an error."));
continue;
}
next.key = QByteArray::fromBase64(
fileHashIt->toString().toUtf8());
} else if (targetIt->toString() == "selfie") {
next.key = QByteArray();
}
result.push_back(std::move(next));
}
return result;
}
EncryptedData EncryptData(bytes::const_span bytes) {
return EncryptData(bytes, GenerateSecretBytes());
}

View File

@ -23,6 +23,17 @@ bytes::vector DecryptSecureSecret(
bytes::vector SerializeData(const std::map<QString, QString> &data);
std::map<QString, QString> DeserializeData(bytes::const_span bytes);
struct DataError {
// QByteArray - bad existing scan with such file_hash
// QString - bad data field value with such key
// base::none - additional scan required
base::optional_variant<QByteArray, QString> key;
QString type; // personal_details, passport, etc.
QString text;
};
std::vector<DataError> DeserializeErrors(bytes::const_span json);
struct EncryptedData {
bytes::vector secret;
bytes::vector hash;

View File

@ -156,12 +156,14 @@ FormRequest::FormRequest(
const QString &scope,
const QString &callbackUrl,
const QString &publicKey,
const QString &payload)
const QString &payload,
const QString &errors)
: botId(botId)
, scope(scope)
, callbackUrl(callbackUrl)
, publicKey(publicKey)
, payload(payload) {
, payload(payload)
, errors(errors) {
}
EditFile::EditFile(
@ -247,8 +249,8 @@ auto FormController::prepareFinalData() -> FinalData {
auto hashes = QVector<MTPSecureValueHash>();
auto secureData = QJsonObject();
const auto addValueToJSON = [&](
const QString &key,
not_null<const Value*> value) {
const QString &key,
not_null<const Value*> value) {
auto object = QJsonObject();
if (!value->data.parsed.fields.empty()) {
object.insert("data", GetJSONFromMap({
@ -279,8 +281,8 @@ auto FormController::prepareFinalData() -> FinalData {
};
const auto scopes = ComputeScopes(this);
for (const auto &scope : scopes) {
const auto ready = ComputeScopeRowReadyString(scope);
if (ready.isEmpty()) {
const auto row = ComputeScopeRow(scope);
if (row.ready.isEmpty() || !row.error.isEmpty()) {
errors.push_back(scope.fields);
continue;
}
@ -489,6 +491,43 @@ void FormController::decryptValues() {
for (auto &[type, value] : _form.values) {
decryptValue(value);
}
fillErrors();
}
void FormController::fillErrors() {
const auto errors = _request.errors.toUtf8();
const auto list = DeserializeErrors(bytes::make_span(errors));
for (const auto &error : list) {
for (auto &[type, value] : _form.values) {
if (ValueCredentialsKey(type) != error.type) {
continue;
}
if (!error.key.has_value()) {
value.scanMissingError = error.text;
} else if (const auto key = base::get_if<QString>(&error.key)) {
value.data.parsed.fields[(*key)].error = error.text;
} else if (auto hash = base::get_if<QByteArray>(&error.key)) {
const auto check = [&](const File &file) {
return *hash == QByteArray::fromRawData(
reinterpret_cast<const char*>(file.hash.data()),
file.hash.size());
};
for (auto &scan : value.scans) {
if (check(scan)) {
scan.error = error.text;
break;
}
}
if (value.selfie) {
if (check(*value.selfie) || hash->isEmpty()) {
value.selfie->error = error.text;
}
}
}
break;
}
}
}
void FormController::decryptValue(Value &value) {

View File

@ -32,13 +32,15 @@ struct FormRequest {
const QString &scope,
const QString &callbackUrl,
const QString &publicKey,
const QString &payload);
const QString &payload,
const QString &errors);
UserId botId;
QString scope;
QString callbackUrl;
QString publicKey;
QString payload;
QString errors;
};
@ -317,6 +319,7 @@ private:
void decryptValue(Value &value);
bool validateValueSecrets(Value &value);
void resetValue(Value &value);
void fillErrors();
void loadFile(File &file);
void fileLoadDone(FileKey key, const QByteArray &bytes);

View File

@ -188,14 +188,28 @@ ScopeRow ComputeScopeRow(const Scope &scope) {
const auto addReadyError = [&](ScopeRow &&row) {
const auto ready = ComputeScopeRowReadyString(scope);
row.ready = ready;
// #TODO passport bot errors
//row.error = scope.fields->error.has_value()
// ? (!scope.fields->error->isEmpty()
// ? *scope.fields->error
// : !ready.isEmpty()
// ? ready
// : row.description)
// : QString();
auto errors = QStringList();
const auto addValueErrors = [&](not_null<const Value*> value) {
for (const auto &scan : value->scans) {
if (!scan.error.isEmpty()) {
errors.push_back(scan.error);
}
}
if (value->selfie && !value->selfie->error.isEmpty()) {
errors.push_back(value->selfie->error);
}
if (!value->scanMissingError.isEmpty()) {
errors.push_back(value->scanMissingError);
}
for (const auto &[key, value] : value->data.parsed.fields) {
if (!value.error.isEmpty()) {
errors.push_back(value.error);
}
}
};
ranges::for_each(scope.documents, addValueErrors);
addValueErrors(scope.fields);
row.error = errors.join('\n');
return row;
};
switch (scope.type) {

View File

@ -565,7 +565,7 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
&& (&file == &*_editDocument->selfieInEdit);
return {
FileKey{ file.fields.id, file.fields.dcId },
status,
!file.fields.error.isEmpty() ? file.fields.error : status,
file.fields.image,
file.deleted,
isSelfie,
@ -871,6 +871,7 @@ void PanelController::editScope(int index, int documentIndex) {
_editDocument->type),
_editValue->data.parsedInEdit,
_editDocument->data.parsedInEdit,
_editDocument->scanMissingError,
valueFiles(*_editDocument),
(_editScope->selfieRequired
? valueSelfie(*_editDocument)

View File

@ -930,10 +930,14 @@ int PanelDetailsRow::resizeGetHeight(int newWidth) {
const auto inputRight = padding.right();
const auto inputWidth = std::max(newWidth - inputLeft - inputRight, 0);
const auto innerHeight = resizeInner(inputLeft, inputTop, inputWidth);
return padding.top()
const auto result = padding.top()
+ innerHeight
+ (_error ? _error->height() : 0)
+ padding.bottom();
if (_error) {
_error->moveToLeft(inputLeft, result - _error->height());
}
return result;
}
void PanelDetailsRow::showError(const QString &error) {
@ -959,12 +963,18 @@ void PanelDetailsRow::showError(const QString &error) {
} else {
_error->entity()->setText(error);
}
_error->heightValue(
) | rpl::start_with_next([=] {
resizeToWidth(width());
}, _error->lifetime());
_error->show(anim::type::normal);
} else if (_error) {
_error->hide(anim::type::normal);
}
}
bool PanelDetailsRow::errorShown() const {
return _errorShown;
}
void PanelDetailsRow::hideError() {
startErrorAnimation(false);
if (_error) {

View File

@ -66,6 +66,7 @@ public:
virtual rpl::producer<QString> value() const = 0;
virtual QString valueCurrent() const = 0;
void showError(const QString &error);
bool errorShown() const;
void hideError();
void finishAnimating();

View File

@ -211,6 +211,7 @@ PanelEditDocument::PanelEditDocument(
Scheme scheme,
const ValueMap &data,
const ValueMap &scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie)
: _controller(controller)
@ -222,7 +223,12 @@ PanelEditDocument::PanelEditDocument(
this,
langFactory(lng_passport_save_value),
st::passportPanelSaveValue) {
setupControls(data, &scanData, std::move(files), std::move(selfie));
setupControls(
data,
&scanData,
missingScansError,
std::move(files),
std::move(selfie));
}
PanelEditDocument::PanelEditDocument(
@ -239,17 +245,19 @@ PanelEditDocument::PanelEditDocument(
this,
langFactory(lng_passport_save_value),
st::passportPanelSaveValue) {
setupControls(data, nullptr, {}, nullptr);
setupControls(data, nullptr, QString(), {}, nullptr);
}
void PanelEditDocument::setupControls(
const ValueMap &data,
const ValueMap *scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie) {
const auto inner = setupContent(
data,
scanData,
missingScansError,
std::move(files),
std::move(selfie));
@ -267,6 +275,7 @@ void PanelEditDocument::setupControls(
not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
const ValueMap &data,
const ValueMap *scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie) {
const auto inner = _scroll->setOwnedWidget(
@ -282,6 +291,7 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
inner,
_controller,
_scheme.scansHeader,
missingScansError,
std::move(files),
std::move(selfie)));
} else {
@ -401,13 +411,16 @@ bool PanelEditDocument::validate() {
auto first = QPointer<PanelDetailsRow>();
for (const auto [i, field] : base::reversed(_details)) {
const auto &row = _scheme.rows[i];
if (row.validate && !row.validate(field->valueCurrent())) {
if (field->errorShown()
|| (row.validate && !row.validate(field->valueCurrent()))) {
field->showError(QString());
first = field;
}
}
if (!first) {
return !error;
if (error) {
return false;
} else if (!first) {
return true;
}
const auto firsttop = first->mapToGlobal(QPoint(0, 0));
const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));

View File

@ -62,6 +62,7 @@ public:
Scheme scheme,
const ValueMap &data,
const ValueMap &scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie);
PanelEditDocument(
@ -81,11 +82,13 @@ private:
void setupControls(
const ValueMap &data,
const ValueMap *scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie);
not_null<Ui::RpWidget*> setupContent(
const ValueMap &data,
const ValueMap *scanData,
const QString &missingScansError,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie);
void updateControlsGeometry();

View File

@ -31,11 +31,13 @@ public:
const style::PassportScanRow &st,
const QString &name,
const QString &status,
bool deleted);
bool deleted,
bool error);
void setImage(const QImage &image);
void setStatus(const QString &status);
void setDeleted(bool deleted);
void setError(bool error);
rpl::producer<> deleteClicks() const {
return _delete->entity()->clicks();
@ -57,6 +59,7 @@ private:
Text _status;
int _nameHeight = 0;
int _statusHeight = 0;
bool _error = false;
QImage _image;
object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _delete;
object_ptr<Ui::FadeWrapScaled<Ui::RoundButton>> _restore;
@ -68,7 +71,8 @@ ScanButton::ScanButton(
const style::PassportScanRow &st,
const QString &name,
const QString &status,
bool deleted)
bool deleted,
bool error)
: AbstractButton(parent)
, _st(st)
, _name(
@ -79,6 +83,7 @@ ScanButton::ScanButton(
st::defaultTextStyle,
status,
Ui::NameTextOptions())
, _error(error)
, _delete(this, object_ptr<Ui::IconButton>(this, _st.remove))
, _restore(
this,
@ -109,6 +114,11 @@ void ScanButton::setDeleted(bool deleted) {
update();
}
void ScanButton::setError(bool error) {
_error = error;
update();
}
int ScanButton::resizeGetHeight(int newWidth) {
_nameHeight = st::semiboldFont->height;
_statusHeight = st::normalFont->height;
@ -145,7 +155,8 @@ void ScanButton::paintEvent(QPaintEvent *e) {
_st.border,
_st.borderFg);
if (_restore->toggled()) {
const auto deleted = _restore->toggled();
if (deleted) {
p.setOpacity(st::passportScanDeletedOpacity);
}
@ -173,7 +184,9 @@ void ScanButton::paintEvent(QPaintEvent *e) {
top + _st.nameTop,
availableWidth,
width());
p.setPen(st::windowSubTextFg);
p.setPen((_error && !deleted)
? st::boxTextFgError
: st::windowSubTextFg);
_status.drawLeftElided(
p,
left + _st.textLeft,
@ -186,26 +199,56 @@ EditScans::EditScans(
QWidget *parent,
not_null<PanelController*> controller,
const QString &header,
const QString &errorMissing,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie)
: RpWidget(parent)
, _controller(controller)
, _files(std::move(files))
, _selfie(std::move(selfie))
, _initialCount(_files.size())
, _errorMissing(errorMissing)
, _content(this) {
setupContent(header);
}
bool EditScans::uploadedSomeMore() const {
const auto from = begin(_files) + _initialCount;
const auto till = end(_files);
return std::find_if(from, till, [](const ScanInfo &file) {
return !file.deleted;
}) != till;
}
base::optional<int> EditScans::validateGetErrorTop() {
const auto exists = ranges::find(
const auto exists = ranges::find_if(
_files,
false,
[](const ScanInfo &file) { return file.deleted; }) != end(_files);
if (!exists) {
[](const ScanInfo &file) { return !file.deleted; }) != end(_files);
const auto errorExists = ranges::find_if(
_files,
[](const ScanInfo &file) { return !file.error.isEmpty(); }
) != end(_files);
if (!exists
|| ((errorExists || _uploadMoreError) && !uploadedSomeMore())) {
toggleError(true);
return (_files.size() > 5) ? _upload->y() : _header->y();
}
if (_selfie && (!_selfie->key.id || _selfie->deleted)) {
const auto nonDeletedErrorIt = ranges::find_if(
_files,
[](const ScanInfo &file) {
return !file.error.isEmpty() && !file.deleted;
});
if (nonDeletedErrorIt != end(_files)) {
const auto index = (nonDeletedErrorIt - begin(_files));
toggleError(true);
return _rows[index]->y();
}
if (_selfie
&& (!_selfie->key.id
|| _selfie->deleted
|| !_selfie->error.isEmpty())) {
toggleSelfieError(true);
return _selfieHeader->y();
}
@ -234,7 +277,18 @@ void EditScans::setupContent(const QString &header) {
st::passportFormHeader),
st::passportUploadHeaderPadding));
_header->toggle(!_files.empty(), anim::type::instant);
if (!_errorMissing.isEmpty()) {
_uploadMoreError = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
_errorMissing,
Ui::FlatLabel::InitType::Simple,
st::passportVerifyErrorLabel),
st::passportUploadErrorPadding));
_uploadMoreError->toggle(true, anim::type::instant);
}
_wrap = inner->add(object_ptr<Ui::VerticalLayout>(inner));
for (const auto &scan : _files) {
pushScan(scan);
@ -317,6 +371,7 @@ void EditScans::updateScan(ScanInfo &&info) {
button->setStatus(info.status);
button->setImage(info.thumb);
button->setDeleted(info.deleted);
button->setError(!info.error.isEmpty());
};
if (info.selfie) {
Assert(info.key.id != 0);
@ -413,7 +468,8 @@ base::unique_qptr<Ui::SlideWrap<ScanButton>> EditScans::createScan(
st::passportScanRow,
name,
info.status,
info.deleted))));
info.deleted,
!info.error.isEmpty()))));
result->entity()->setImage(info.thumb);
return result;
}

View File

@ -36,6 +36,7 @@ public:
QWidget *parent,
not_null<PanelController*> controller,
const QString &header,
const QString &errorMissing,
std::vector<ScanInfo> &&files,
std::unique_ptr<ScanInfo> &&selfie);
@ -62,6 +63,7 @@ private:
void toggleError(bool shown);
void hideError();
void errorAnimationCallback();
bool uploadedSomeMore() const;
void toggleSelfieError(bool shown);
void hideSelfieError();
@ -70,10 +72,13 @@ private:
not_null<PanelController*> _controller;
std::vector<ScanInfo> _files;
std::unique_ptr<ScanInfo> _selfie;
int _initialCount = 0;
QString _errorMissing;
object_ptr<Ui::VerticalLayout> _content;
QPointer<Ui::SlideWrap<BoxContentDivider>> _divider;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _header;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _uploadMoreError;
QPointer<Ui::VerticalLayout> _wrap;
std::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> _rows;
QPointer<Info::Profile::Button> _upload;

View File

@ -74,8 +74,13 @@ void PanelForm::Row::updateContent(
_description.setText(
st::defaultTextStyle,
description,
Ui::NameTextOptions());
_ready = ready;
TextParseOptions {
TextParseMultiline,
0,
0,
Qt::LayoutDirectionAuto
});
_ready = ready && !error;
if (_error != error) {
_error = error;
if (animated == anim::type::instant) {