tdesktop/Telegram/SourceFiles/intro/intro_qr.cpp

437 lines
12 KiB
C++
Raw Normal View History

2019-11-22 09:40:52 +00:00
/*
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 "intro/intro_qr.h"
2019-11-27 09:45:23 +00:00
#include "intro/intro_phone.h"
2019-12-13 15:00:21 +00:00
#include "intro/intro_widget.h"
#include "intro/intro_password_check.h"
2019-11-26 11:10:44 +00:00
#include "lang/lang_keys.h"
2019-11-22 09:40:52 +00:00
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
2019-11-26 11:10:44 +00:00
#include "ui/wrap/vertical_layout.h"
2019-11-26 13:17:19 +00:00
#include "ui/effects/radial_animation.h"
2019-11-26 11:10:44 +00:00
#include "ui/text/text_utilities.h"
2019-11-26 11:39:54 +00:00
#include "ui/image/image_prepare.h"
2019-11-26 12:56:07 +00:00
#include "ui/painter.h"
2019-11-22 09:40:52 +00:00
#include "main/main_account.h"
#include "boxes/confirm_box.h"
#include "core/application.h"
2019-12-13 15:00:21 +00:00
#include "core/core_cloud_password.h"
#include "core/update_checker.h"
2019-11-22 09:40:52 +00:00
#include "base/unixtime.h"
#include "qr/qr_generate.h"
#include "styles/style_intro.h"
namespace Intro {
2019-11-26 11:10:44 +00:00
namespace details {
2019-11-22 09:40:52 +00:00
namespace {
2019-11-26 12:56:07 +00:00
[[nodiscard]] QImage TelegramLogoImage() {
const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
2019-11-26 11:10:44 +00:00
result.fill(Qt::transparent);
2019-11-26 12:56:07 +00:00
result.setDevicePixelRatio(style::DevicePixelRatio());
2019-11-26 11:10:44 +00:00
{
2019-11-26 12:56:07 +00:00
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::activeButtonBg);
p.setPen(Qt::NoPen);
p.drawEllipse(QRect(QPoint(), size));
st::introQrPlane.paintInCenter(p, QRect(QPoint(), size));
2019-11-26 11:10:44 +00:00
}
return result;
2019-11-22 09:40:52 +00:00
}
[[nodiscard]] QImage TelegramQrExact(const Qr::Data &data, int pixel) {
2019-11-26 11:39:54 +00:00
return Qr::Generate(data, pixel, st::windowFg->c);
2019-11-22 09:40:52 +00:00
}
[[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max = 0) {
Expects(data.size > 0);
if (max > 0 && data.size * pixel > max) {
pixel = std::max(max / data.size, 1);
}
2019-11-26 14:27:09 +00:00
const auto qr = TelegramQrExact(data, pixel * style::DevicePixelRatio());
auto result = QImage(qr.size(), QImage::Format_ARGB32_Premultiplied);
result.fill(st::windowBg->c);
{
auto p = QPainter(&result);
p.drawImage(QRect(QPoint(), qr.size()), qr);
}
return result;
2019-11-22 09:40:52 +00:00
}
[[nodiscard]] not_null<Ui::RpWidget*> PrepareQrWidget(
not_null<QWidget*> parent,
2019-11-26 11:10:44 +00:00
rpl::producer<QByteArray> codes) {
2019-11-26 13:17:19 +00:00
struct State {
explicit State(Fn<void()> callback)
: waiting(callback, st::defaultInfiniteRadialAnimation) {
}
2019-11-26 14:27:09 +00:00
QImage previous;
2019-11-26 13:17:19 +00:00
QImage qr;
QImage center;
2019-11-26 14:27:09 +00:00
Ui::Animations::Simple shown;
2019-11-26 13:17:19 +00:00
Ui::InfiniteRadialAnimation waiting;
};
2019-11-26 11:39:54 +00:00
auto qrs = std::move(
2019-11-26 11:10:44 +00:00
codes
) | rpl::map([](const QByteArray &code) {
2019-11-26 11:39:54 +00:00
return Qr::Encode(code, Qr::Redundancy::Quartile);
});
2019-11-26 12:56:07 +00:00
auto palettes = rpl::single(
rpl::empty_value()
) | rpl::then(
style::PaletteChanged()
);
2019-11-26 11:39:54 +00:00
auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
2019-11-26 13:17:19 +00:00
const auto state = result->lifetime().make_state<State>(
[=] { result->update(); });
state->waiting.start();
2019-11-26 12:56:07 +00:00
result->resize(st::introQrMaxSize, st::introQrMaxSize);
2019-11-26 11:39:54 +00:00
rpl::combine(
std::move(qrs),
2019-11-26 12:56:07 +00:00
rpl::duplicate(palettes)
2019-11-26 11:39:54 +00:00
) | rpl::map([](const Qr::Data &code, const auto &) {
2019-11-26 11:10:44 +00:00
return TelegramQr(code, st::introQrPixel, st::introQrMaxSize);
}) | rpl::start_with_next([=](QImage &&image) {
2019-11-26 14:27:09 +00:00
state->previous = std::move(state->qr);
2019-11-26 13:17:19 +00:00
state->qr = std::move(image);
state->waiting.stop();
2019-11-26 14:27:09 +00:00
state->shown.stop();
state->shown.start(
[=] { result->update(); },
0.,
1.,
st::fadeWrapDuration);
2019-11-22 09:40:52 +00:00
}, result->lifetime());
2019-11-26 12:56:07 +00:00
std::move(
palettes
) | rpl::map([] {
return TelegramLogoImage();
}) | rpl::start_with_next([=](QImage &&image) {
2019-11-26 13:17:19 +00:00
state->center = std::move(image);
2019-11-26 12:56:07 +00:00
}, result->lifetime());
2019-11-22 09:40:52 +00:00
result->paintRequest(
2019-11-26 13:17:19 +00:00
) | rpl::start_with_next([=](QRect clip) {
2019-11-26 12:56:07 +00:00
auto p = QPainter(result);
2019-11-26 14:27:09 +00:00
const auto shown = state->qr.isNull() ? 0. : state->shown.value(1.);
if (!state->qr.isNull()) {
const auto size = state->qr.size() / cIntRetinaFactor();
const auto qr = QRect(
(result->width() - size.width()) / 2,
(result->height() - size.height()) / 2,
size.width(),
size.height());
if (shown == 1.) {
state->previous = QImage();
} else if (!state->previous.isNull()) {
p.drawImage(qr, state->previous);
}
p.setOpacity(shown);
p.drawImage(qr, state->qr);
p.setOpacity(1.);
}
const auto rect = QRect(
2019-11-26 13:17:19 +00:00
(result->width() - st::introQrCenterSize) / 2,
(result->height() - st::introQrCenterSize) / 2,
st::introQrCenterSize,
st::introQrCenterSize);
p.drawImage(rect, state->center);
if (!anim::Disabled() && state->waiting.animating()) {
auto hq = PainterHighQualityEnabler(p);
const auto line = st::radialLine;
const auto radial = state->waiting.computeState();
auto pen = st::activeButtonBg->p;
pen.setWidth(line);
pen.setCapStyle(Qt::RoundCap);
2019-11-26 14:27:09 +00:00
p.setOpacity(radial.shown * (1. - shown));
2019-11-26 13:17:19 +00:00
p.setPen(pen);
p.drawArc(
rect.marginsAdded({ line, line, line, line }),
radial.arcFrom,
radial.arcLength);
p.setOpacity(1.);
}
2019-11-22 09:40:52 +00:00
}, result->lifetime());
return result;
}
} // namespace
QrWidget::QrWidget(
QWidget *parent,
not_null<Main::Account*> account,
2019-11-26 11:10:44 +00:00
not_null<Data*> data)
2019-11-26 11:39:54 +00:00
: Step(parent, account, data)
2019-11-27 08:02:56 +00:00
, _api(account->mtp())
2019-11-26 14:27:09 +00:00
, _refreshTimer([=] { refreshCode(); }) {
2019-11-26 11:10:44 +00:00
setTitleText(rpl::single(QString()));
setDescriptionText(rpl::single(QString()));
2019-11-22 09:40:52 +00:00
setErrorCentered(true);
account->mtpUpdates(
) | rpl::start_with_next([=](const MTPUpdates &updates) {
checkForTokenUpdate(updates);
}, lifetime());
2019-11-26 11:10:44 +00:00
setupControls();
2019-11-22 09:40:52 +00:00
refreshCode();
}
2019-11-26 14:27:09 +00:00
int QrWidget::errorTop() const {
return contentTop() + st::introQrErrorTop;
}
2019-11-22 09:40:52 +00:00
void QrWidget::checkForTokenUpdate(const MTPUpdates &updates) {
updates.match([&](const MTPDupdateShort &data) {
checkForTokenUpdate(data.vupdate());
}, [&](const MTPDupdates &data) {
for (const auto &update : data.vupdates().v) {
checkForTokenUpdate(update);
}
}, [&](const MTPDupdatesCombined &data) {
for (const auto &update : data.vupdates().v) {
checkForTokenUpdate(update);
}
}, [](const auto &) {});
}
void QrWidget::checkForTokenUpdate(const MTPUpdate &update) {
update.match([&](const MTPDupdateLoginToken &data) {
if (_requestId) {
_forceRefresh = true;
} else {
_refreshTimer.cancel();
refreshCode();
}
}, [](const auto &) {});
}
void QrWidget::submit() {
goReplace<PhoneWidget>();
}
rpl::producer<QString> QrWidget::nextButtonText() const {
2019-11-26 11:10:44 +00:00
return rpl::single(QString());
}
void QrWidget::setupControls() {
const auto code = PrepareQrWidget(this, _qrCodes.events());
rpl::combine(
sizeValue(),
code->widthValue()
) | rpl::start_with_next([=](QSize size, int codeWidth) {
code->moveToLeft(
(size.width() - codeWidth) / 2,
contentTop() + st::introQrTop);
}, code->lifetime());
const auto title = Ui::CreateChild<Ui::FlatLabel>(
this,
tr::lng_intro_qr_title(),
st::introQrTitle);
rpl::combine(
sizeValue(),
title->widthValue()
) | rpl::start_with_next([=](QSize size, int titleWidth) {
title->resizeToWidth(st::introQrTitleWidth);
const auto oneLine = st::introQrTitle.style.font->height;
const auto topDelta = (title->height() - oneLine);
2019-11-26 11:10:44 +00:00
title->moveToLeft(
(size.width() - title->width()) / 2,
contentTop() + st::introQrTitleTop - topDelta);
2019-11-26 11:10:44 +00:00
}, title->lifetime());
const auto steps = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto texts = {
tr::lng_intro_qr_step1,
tr::lng_intro_qr_step2,
tr::lng_intro_qr_step3,
};
auto index = 0;
2019-11-26 11:10:44 +00:00
for (const auto &text : texts) {
const auto label = steps->add(
2019-11-26 11:10:44 +00:00
object_ptr<Ui::FlatLabel>(
steps,
2019-11-26 11:10:44 +00:00
text(Ui::Text::RichLangValue),
st::introQrStep),
st::introQrStepMargins);
const auto number = Ui::CreateChild<Ui::FlatLabel>(
steps,
rpl::single(Ui::Text::Bold(QString::number(++index) + ".")),
st::defaultFlatLabel);
rpl::combine(
number->widthValue(),
label->positionValue()
) | rpl::start_with_next([=](int width, QPoint position) {
number->moveToLeft(
position.x() - width - st::normalFont->spacew,
position.y());
}, number->lifetime());
2019-11-26 11:10:44 +00:00
}
steps->resizeToWidth(st::introQrLabelsWidth);
rpl::combine(
sizeValue(),
steps->widthValue()
) | rpl::start_with_next([=](QSize size, int stepsWidth) {
steps->moveToLeft(
(size.width() - stepsWidth) / 2,
contentTop() + st::introQrStepsTop);
}, steps->lifetime());
const auto skip = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_intro_qr_skip(tr::now));
rpl::combine(
sizeValue(),
skip->widthValue()
) | rpl::start_with_next([=](QSize size, int skipWidth) {
skip->moveToLeft(
(size.width() - skipWidth) / 2,
contentTop() + st::introQrSkipTop);
}, skip->lifetime());
2019-11-26 14:27:09 +00:00
skip->setClickedCallback([=] { submit(); });
2019-11-22 09:40:52 +00:00
}
void QrWidget::refreshCode() {
if (_requestId) {
return;
}
_requestId = _api.request(MTPauth_ExportLoginToken(
MTP_int(ApiId),
MTP_string(ApiHash),
MTP_vector<MTPint>(0)
)).done([=](const MTPauth_LoginToken &result) {
handleTokenResult(result);
}).fail([=](const RPCError &error) {
showTokenError(error);
}).send();
}
void QrWidget::handleTokenResult(const MTPauth_LoginToken &result) {
result.match([&](const MTPDauth_loginToken &data) {
_requestId = 0;
showToken(data.vtoken().v);
if (base::take(_forceRefresh)) {
refreshCode();
} else {
const auto left = data.vexpires().v - base::unixtime::now();
_refreshTimer.callOnce(std::max(left, 1) * crl::time(1000));
}
}, [&](const MTPDauth_loginTokenMigrateTo &data) {
importTo(data.vdc_id().v, data.vtoken().v);
}, [&](const MTPDauth_loginTokenSuccess &data) {
done(data.vauthorization());
});
}
void QrWidget::showTokenError(const RPCError &error) {
_requestId = 0;
2019-12-13 15:00:21 +00:00
if (error.type() == qstr("SESSION_PASSWORD_NEEDED")) {
sendCheckPasswordRequest();
} else if (base::take(_forceRefresh)) {
2019-11-22 09:40:52 +00:00
refreshCode();
} else {
showError(rpl::single(error.type()));
}
}
void QrWidget::showToken(const QByteArray &token) {
const auto encoded = token.toBase64(QByteArray::Base64UrlEncoding);
_qrCodes.fire_copy("tg://login?token=" + encoded);
2019-11-22 09:40:52 +00:00
}
void QrWidget::importTo(MTP::DcId dcId, const QByteArray &token) {
Expects(_requestId != 0);
2019-12-13 15:11:31 +00:00
_api.instance()->setMainDcId(dcId);
2019-11-22 09:40:52 +00:00
_requestId = _api.request(MTPauth_ImportLoginToken(
MTP_bytes(token)
)).done([=](const MTPauth_LoginToken &result) {
handleTokenResult(result);
}).fail([=](const RPCError &error) {
showTokenError(error);
}).toDC(dcId).send();
}
void QrWidget::done(const MTPauth_Authorization &authorization) {
authorization.match([&](const MTPDauth_authorization &data) {
if (data.vuser().type() != mtpc_user
|| !data.vuser().c_user().is_self()) {
showError(rpl::single(Lang::Hard::ServerError()));
return;
}
const auto phone = data.vuser().c_user().vphone().value_or_empty();
cSetLoggedPhoneNumber(phone);
finish(data.vuser());
}, [&](const MTPDauth_authorizationSignUpRequired &data) {
_requestId = 0;
LOG(("API Error: Unexpected auth.authorizationSignUpRequired."));
showError(rpl::single(Lang::Hard::ServerError()));
});
}
2019-12-13 15:00:21 +00:00
void QrWidget::sendCheckPasswordRequest() {
_requestId = _api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
result.match([&](const MTPDaccount_password &data) {
getData()->pwdRequest = Core::ParseCloudPasswordCheckRequest(
data);
if (!data.vcurrent_algo() || !data.vsrp_id() || !data.vsrp_B()) {
LOG(("API Error: No current password received on login."));
goReplace<QrWidget>();
return;
} else if (!getData()->pwdRequest) {
const auto box = std::make_shared<QPointer<Ui::BoxContent>>();
const auto callback = [=] {
Core::UpdateApplication();
if (*box) (*box)->closeBox();
};
*box = Ui::show(Box<ConfirmBox>(
tr::lng_passport_app_out_of_date(tr::now),
tr::lng_menu_update(tr::now),
callback));
return;
}
getData()->hasRecovery = data.is_has_recovery();
getData()->pwdHint = qs(data.vhint().value_or_empty());
getData()->pwdNotEmptyPassport = data.is_has_secure_values();
goReplace<PasswordCheckWidget>();
});
}).fail([=](const RPCError &error) {
showTokenError(error);
}).send();
}
2019-11-22 09:40:52 +00:00
void QrWidget::activate() {
Step::activate();
2019-11-26 11:10:44 +00:00
showChildren();
2019-11-22 09:40:52 +00:00
}
void QrWidget::finished() {
Step::finished();
_refreshTimer.cancel();
rpcInvalidate();
cancelled();
}
void QrWidget::cancelled() {
_api.request(base::take(_requestId)).cancel();
}
2019-11-26 11:10:44 +00:00
} // namespace details
2019-11-22 09:40:52 +00:00
} // namespace Intro