intro_qr.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "intro/intro_qr.h"
  8. #include "boxes/abstract_box.h"
  9. #include "intro/intro_phone.h"
  10. #include "intro/intro_widget.h"
  11. #include "intro/intro_password_check.h"
  12. #include "lang/lang_keys.h"
  13. #include "ui/widgets/buttons.h"
  14. #include "ui/widgets/labels.h"
  15. #include "ui/wrap/fade_wrap.h"
  16. #include "ui/wrap/vertical_layout.h"
  17. #include "ui/effects/radial_animation.h"
  18. #include "ui/text/text_utilities.h"
  19. #include "ui/image/image_prepare.h"
  20. #include "ui/painter.h"
  21. #include "main/main_account.h"
  22. #include "ui/boxes/confirm_box.h"
  23. #include "core/application.h"
  24. #include "core/core_cloud_password.h"
  25. #include "core/update_checker.h"
  26. #include "base/unixtime.h"
  27. #include "qr/qr_generate.h"
  28. #include "styles/style_intro.h"
  29. namespace Intro {
  30. namespace details {
  31. namespace {
  32. [[nodiscard]] QImage TelegramQrExact(const Qr::Data &data, int pixel) {
  33. return Qr::Generate(data, pixel, Qt::black);
  34. }
  35. [[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max = 0) {
  36. Expects(data.size > 0);
  37. if (max > 0 && data.size * pixel > max) {
  38. pixel = std::max(max / data.size, 1);
  39. }
  40. const auto qr = TelegramQrExact(data, pixel * style::DevicePixelRatio());
  41. auto result = QImage(qr.size(), QImage::Format_ARGB32_Premultiplied);
  42. result.fill(Qt::white);
  43. {
  44. auto p = QPainter(&result);
  45. p.drawImage(QRect(QPoint(), qr.size()), qr);
  46. }
  47. return result;
  48. }
  49. [[nodiscard]] QColor QrActiveColor() {
  50. return QColor(0x40, 0xA7, 0xE3); // Default windowBgActive.
  51. }
  52. [[nodiscard]] not_null<Ui::RpWidget*> PrepareQrWidget(
  53. not_null<QWidget*> parent,
  54. rpl::producer<QByteArray> codes) {
  55. struct State {
  56. explicit State(Fn<void()> callback)
  57. : waiting(callback, st::defaultInfiniteRadialAnimation) {
  58. }
  59. QImage previous;
  60. QImage qr;
  61. QImage center;
  62. Ui::Animations::Simple shown;
  63. Ui::InfiniteRadialAnimation waiting;
  64. };
  65. auto qrs = std::move(
  66. codes
  67. ) | rpl::map([](const QByteArray &code) {
  68. return Qr::Encode(code, Qr::Redundancy::Quartile);
  69. });
  70. auto palettes = rpl::single(rpl::empty) | rpl::then(
  71. style::PaletteChanged()
  72. );
  73. auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
  74. const auto state = result->lifetime().make_state<State>(
  75. [=] { result->update(); });
  76. state->waiting.start();
  77. const auto size = st::introQrMaxSize + 2 * st::introQrBackgroundSkip;
  78. result->resize(size, size);
  79. rpl::combine(
  80. std::move(qrs),
  81. rpl::duplicate(palettes)
  82. ) | rpl::map([](const Qr::Data &code, const auto &) {
  83. return TelegramQr(code, st::introQrPixel, st::introQrMaxSize);
  84. }) | rpl::start_with_next([=](QImage &&image) {
  85. state->previous = std::move(state->qr);
  86. state->qr = std::move(image);
  87. state->waiting.stop();
  88. state->shown.stop();
  89. state->shown.start(
  90. [=] { result->update(); },
  91. 0.,
  92. 1.,
  93. st::fadeWrapDuration);
  94. }, result->lifetime());
  95. std::move(
  96. palettes
  97. ) | rpl::map([] {
  98. return TelegramLogoImage();
  99. }) | rpl::start_with_next([=](QImage &&image) {
  100. state->center = std::move(image);
  101. }, result->lifetime());
  102. result->paintRequest(
  103. ) | rpl::start_with_next([=](QRect clip) {
  104. auto p = QPainter(result);
  105. const auto has = !state->qr.isNull();
  106. const auto shown = has ? state->shown.value(1.) : 0.;
  107. const auto usualSize = 41;
  108. const auto pixel = std::clamp(
  109. st::introQrMaxSize / usualSize,
  110. 1,
  111. st::introQrPixel);
  112. const auto size = has
  113. ? (state->qr.size() / style::DevicePixelRatio())
  114. : QSize(usualSize * pixel, usualSize * pixel);
  115. const auto qr = QRect(
  116. (result->width() - size.width()) / 2,
  117. (result->height() - size.height()) / 2,
  118. size.width(),
  119. size.height());
  120. const auto radius = st::introQrBackgroundRadius;
  121. const auto skip = st::introQrBackgroundSkip;
  122. auto hq = PainterHighQualityEnabler(p);
  123. p.setPen(Qt::NoPen);
  124. p.setBrush(Qt::white);
  125. p.drawRoundedRect(
  126. qr.marginsAdded({ skip, skip, skip, skip }),
  127. radius,
  128. radius);
  129. if (!state->qr.isNull()) {
  130. if (shown == 1.) {
  131. state->previous = QImage();
  132. } else if (!state->previous.isNull()) {
  133. p.drawImage(qr, state->previous);
  134. }
  135. p.setOpacity(shown);
  136. p.drawImage(qr, state->qr);
  137. p.setOpacity(1.);
  138. }
  139. const auto rect = QRect(
  140. (result->width() - st::introQrCenterSize) / 2,
  141. (result->height() - st::introQrCenterSize) / 2,
  142. st::introQrCenterSize,
  143. st::introQrCenterSize);
  144. p.drawImage(rect, state->center);
  145. if (!anim::Disabled() && state->waiting.animating()) {
  146. auto hq = PainterHighQualityEnabler(p);
  147. const auto line = st::radialLine;
  148. const auto radial = state->waiting.computeState();
  149. auto pen = QPen(QrActiveColor());
  150. pen.setWidth(line);
  151. pen.setCapStyle(Qt::RoundCap);
  152. p.setOpacity(radial.shown * (1. - shown));
  153. p.setPen(pen);
  154. p.drawArc(
  155. rect.marginsAdded({ line, line, line, line }),
  156. radial.arcFrom,
  157. radial.arcLength);
  158. p.setOpacity(1.);
  159. }
  160. }, result->lifetime());
  161. return result;
  162. }
  163. } // namespace
  164. QrWidget::QrWidget(
  165. QWidget *parent,
  166. not_null<Main::Account*> account,
  167. not_null<Data*> data)
  168. : Step(parent, account, data)
  169. , _refreshTimer([=] { refreshCode(); }) {
  170. setTitleText(rpl::single(QString()));
  171. setDescriptionText(rpl::single(QString()));
  172. setErrorCentered(true);
  173. cancelNearestDcRequest();
  174. account->mtpUpdates(
  175. ) | rpl::start_with_next([=](const MTPUpdates &updates) {
  176. checkForTokenUpdate(updates);
  177. }, lifetime());
  178. setupControls();
  179. account->mtp().mainDcIdValue(
  180. ) | rpl::start_with_next([=] {
  181. api().request(base::take(_requestId)).cancel();
  182. refreshCode();
  183. }, lifetime());
  184. }
  185. int QrWidget::errorTop() const {
  186. return contentTop() + st::introQrErrorTop;
  187. }
  188. void QrWidget::checkForTokenUpdate(const MTPUpdates &updates) {
  189. updates.match([&](const MTPDupdateShort &data) {
  190. checkForTokenUpdate(data.vupdate());
  191. }, [&](const MTPDupdates &data) {
  192. for (const auto &update : data.vupdates().v) {
  193. checkForTokenUpdate(update);
  194. }
  195. }, [&](const MTPDupdatesCombined &data) {
  196. for (const auto &update : data.vupdates().v) {
  197. checkForTokenUpdate(update);
  198. }
  199. }, [](const auto &) {});
  200. }
  201. void QrWidget::checkForTokenUpdate(const MTPUpdate &update) {
  202. update.match([&](const MTPDupdateLoginToken &data) {
  203. if (_requestId) {
  204. _forceRefresh = true;
  205. } else {
  206. _refreshTimer.cancel();
  207. refreshCode();
  208. }
  209. }, [](const auto &) {});
  210. }
  211. void QrWidget::submit() {
  212. goReplace<PhoneWidget>(Animate::Forward);
  213. }
  214. rpl::producer<QString> QrWidget::nextButtonText() const {
  215. return rpl::single(QString());
  216. }
  217. void QrWidget::setupControls() {
  218. const auto code = PrepareQrWidget(this, _qrCodes.events());
  219. rpl::combine(
  220. sizeValue(),
  221. code->widthValue()
  222. ) | rpl::start_with_next([=](QSize size, int codeWidth) {
  223. code->moveToLeft(
  224. (size.width() - codeWidth) / 2,
  225. contentTop() + st::introQrTop);
  226. }, code->lifetime());
  227. const auto title = Ui::CreateChild<Ui::FlatLabel>(
  228. this,
  229. tr::lng_intro_qr_title(),
  230. st::introQrTitle);
  231. rpl::combine(
  232. sizeValue(),
  233. title->widthValue()
  234. ) | rpl::start_with_next([=](QSize size, int titleWidth) {
  235. title->resizeToWidth(st::introQrTitleWidth);
  236. const auto oneLine = st::introQrTitle.style.font->height;
  237. const auto topDelta = (title->height() - oneLine);
  238. title->moveToLeft(
  239. (size.width() - title->width()) / 2,
  240. contentTop() + st::introQrTitleTop - topDelta);
  241. }, title->lifetime());
  242. const auto steps = Ui::CreateChild<Ui::VerticalLayout>(this);
  243. const auto texts = {
  244. tr::lng_intro_qr_step1,
  245. tr::lng_intro_qr_step2,
  246. tr::lng_intro_qr_step3,
  247. };
  248. auto index = 0;
  249. for (const auto &text : texts) {
  250. const auto label = steps->add(
  251. object_ptr<Ui::FlatLabel>(
  252. steps,
  253. text(Ui::Text::RichLangValue),
  254. st::introQrStep),
  255. st::introQrStepMargins);
  256. const auto number = Ui::CreateChild<Ui::FlatLabel>(
  257. steps,
  258. rpl::single(Ui::Text::Semibold(QString::number(++index) + ".")),
  259. st::defaultFlatLabel);
  260. rpl::combine(
  261. number->widthValue(),
  262. label->positionValue()
  263. ) | rpl::start_with_next([=](int width, QPoint position) {
  264. number->moveToLeft(
  265. position.x() - width - st::normalFont->spacew,
  266. position.y());
  267. }, number->lifetime());
  268. }
  269. steps->resizeToWidth(st::introQrLabelsWidth);
  270. rpl::combine(
  271. sizeValue(),
  272. steps->widthValue()
  273. ) | rpl::start_with_next([=](QSize size, int stepsWidth) {
  274. steps->moveToLeft(
  275. (size.width() - stepsWidth) / 2,
  276. contentTop() + st::introQrStepsTop);
  277. }, steps->lifetime());
  278. const auto skip = Ui::CreateChild<Ui::LinkButton>(
  279. this,
  280. tr::lng_intro_qr_skip(tr::now));
  281. rpl::combine(
  282. sizeValue(),
  283. skip->widthValue()
  284. ) | rpl::start_with_next([=](QSize size, int skipWidth) {
  285. skip->moveToLeft(
  286. (size.width() - skipWidth) / 2,
  287. contentTop() + st::introQrSkipTop);
  288. }, skip->lifetime());
  289. skip->setClickedCallback([=] { submit(); });
  290. }
  291. void QrWidget::refreshCode() {
  292. if (_requestId) {
  293. return;
  294. }
  295. _requestId = api().request(MTPauth_ExportLoginToken(
  296. MTP_int(ApiId),
  297. MTP_string(ApiHash),
  298. MTP_vector<MTPlong>(0)
  299. )).done([=](const MTPauth_LoginToken &result) {
  300. handleTokenResult(result);
  301. }).fail([=](const MTP::Error &error) {
  302. showTokenError(error);
  303. }).send();
  304. }
  305. void QrWidget::handleTokenResult(const MTPauth_LoginToken &result) {
  306. result.match([&](const MTPDauth_loginToken &data) {
  307. _requestId = 0;
  308. showToken(data.vtoken().v);
  309. if (base::take(_forceRefresh)) {
  310. refreshCode();
  311. } else {
  312. const auto left = data.vexpires().v - base::unixtime::now();
  313. _refreshTimer.callOnce(std::max(left, 1) * crl::time(1000));
  314. }
  315. }, [&](const MTPDauth_loginTokenMigrateTo &data) {
  316. importTo(data.vdc_id().v, data.vtoken().v);
  317. }, [&](const MTPDauth_loginTokenSuccess &data) {
  318. done(data.vauthorization());
  319. });
  320. }
  321. void QrWidget::showTokenError(const MTP::Error &error) {
  322. _requestId = 0;
  323. if (error.type() == u"SESSION_PASSWORD_NEEDED"_q) {
  324. sendCheckPasswordRequest();
  325. } else if (base::take(_forceRefresh)) {
  326. refreshCode();
  327. } else {
  328. showError(rpl::single(error.type()));
  329. }
  330. }
  331. void QrWidget::showToken(const QByteArray &token) {
  332. const auto encoded = token.toBase64(QByteArray::Base64UrlEncoding);
  333. _qrCodes.fire_copy("tg://login?token=" + encoded);
  334. }
  335. void QrWidget::importTo(MTP::DcId dcId, const QByteArray &token) {
  336. Expects(_requestId != 0);
  337. api().instance().setMainDcId(dcId);
  338. _requestId = api().request(MTPauth_ImportLoginToken(
  339. MTP_bytes(token)
  340. )).done([=](const MTPauth_LoginToken &result) {
  341. handleTokenResult(result);
  342. }).fail([=](const MTP::Error &error) {
  343. showTokenError(error);
  344. }).toDC(dcId).send();
  345. }
  346. void QrWidget::done(const MTPauth_Authorization &authorization) {
  347. finish(authorization);
  348. }
  349. void QrWidget::sendCheckPasswordRequest() {
  350. _requestId = api().request(MTPaccount_GetPassword(
  351. )).done([=](const MTPaccount_Password &result) {
  352. result.match([&](const MTPDaccount_password &data) {
  353. getData()->pwdState = Core::ParseCloudPasswordState(data);
  354. if (!data.vcurrent_algo() || !data.vsrp_id() || !data.vsrp_B()) {
  355. LOG(("API Error: No current password received on login."));
  356. goReplace<QrWidget>(Animate::Forward);
  357. return;
  358. } else if (!getData()->pwdState.hasPassword) {
  359. const auto callback = [=](Fn<void()> &&close) {
  360. Core::UpdateApplication();
  361. close();
  362. };
  363. Ui::show(Ui::MakeConfirmBox({
  364. .text = tr::lng_passport_app_out_of_date(),
  365. .confirmed = callback,
  366. .confirmText = tr::lng_menu_update(),
  367. }));
  368. return;
  369. }
  370. goReplace<PasswordCheckWidget>(Animate::Forward);
  371. });
  372. }).fail([=](const MTP::Error &error) {
  373. showTokenError(error);
  374. }).send();
  375. }
  376. void QrWidget::activate() {
  377. Step::activate();
  378. showChildren();
  379. }
  380. void QrWidget::finished() {
  381. Step::finished();
  382. _refreshTimer.cancel();
  383. apiClear();
  384. cancelled();
  385. }
  386. void QrWidget::cancelled() {
  387. api().request(base::take(_requestId)).cancel();
  388. }
  389. QImage TelegramLogoImage() {
  390. const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);
  391. auto result = QImage(
  392. size * style::DevicePixelRatio(),
  393. QImage::Format_ARGB32_Premultiplied);
  394. result.fill(Qt::transparent);
  395. result.setDevicePixelRatio(style::DevicePixelRatio());
  396. {
  397. auto p = QPainter(&result);
  398. auto hq = PainterHighQualityEnabler(p);
  399. p.setBrush(QrActiveColor());
  400. p.setPen(Qt::NoPen);
  401. p.drawEllipse(QRect(QPoint(), size));
  402. st::introQrPlane.paintInCenter(p, QRect(QPoint(), size));
  403. }
  404. return result;
  405. }
  406. } // namespace details
  407. } // namespace Intro