payments_form_summary.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  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 "payments/ui/payments_form_summary.h"
  8. #include "payments/ui/payments_panel_delegate.h"
  9. #include "settings/settings_common.h" // AddButtonWithLabel.
  10. #include "ui/widgets/scroll_area.h"
  11. #include "ui/widgets/buttons.h"
  12. #include "ui/widgets/labels.h"
  13. #include "ui/wrap/vertical_layout.h"
  14. #include "ui/vertical_list.h"
  15. #include "ui/wrap/fade_wrap.h"
  16. #include "ui/text/format_values.h"
  17. #include "ui/text/text_utilities.h"
  18. #include "countries/countries_instance.h"
  19. #include "lang/lang_keys.h"
  20. #include "base/unixtime.h"
  21. #include "styles/style_payments.h"
  22. #include "styles/style_passport.h"
  23. namespace Payments::Ui {
  24. namespace {
  25. constexpr auto kLightOpacity = 0.1;
  26. constexpr auto kLightRippleOpacity = 0.11;
  27. constexpr auto kChosenOpacity = 0.8;
  28. constexpr auto kChosenRippleOpacity = 0.5;
  29. [[nodiscard]] Fn<QColor()> TransparentColor(
  30. const style::color &c,
  31. float64 opacity) {
  32. return [&c, opacity] {
  33. return QColor(
  34. c->c.red(),
  35. c->c.green(),
  36. c->c.blue(),
  37. c->c.alpha() * opacity);
  38. };
  39. }
  40. [[nodiscard]] style::RoundButton TipButtonStyle(
  41. const style::RoundButton &original,
  42. const style::color &light,
  43. const style::color &ripple) {
  44. auto result = original;
  45. result.textBg = light;
  46. result.ripple.color = ripple;
  47. return result;
  48. }
  49. } // namespace
  50. using namespace ::Ui;
  51. class PanelDelegate;
  52. FormSummary::FormSummary(
  53. QWidget *parent,
  54. const Invoice &invoice,
  55. const RequestedInformation &current,
  56. const PaymentMethodDetails &method,
  57. const ShippingOptions &options,
  58. not_null<PanelDelegate*> delegate,
  59. int scrollTop)
  60. : _delegate(delegate)
  61. , _invoice(invoice)
  62. , _method(method)
  63. , _options(options)
  64. , _information(current)
  65. , _scroll(this, st::passportPanelScroll)
  66. , _layout(_scroll->setOwnedWidget(object_ptr<VerticalLayout>(this)))
  67. , _topShadow(this)
  68. , _bottomShadow(this)
  69. , _submit(_invoice.receipt.paid
  70. ? object_ptr<RoundButton>(nullptr)
  71. : object_ptr<RoundButton>(
  72. this,
  73. tr::lng_payments_pay_amount(
  74. lt_amount,
  75. rpl::single(formatAmount(computeTotalAmount()))),
  76. st::paymentsPanelSubmit))
  77. , _cancel(
  78. this,
  79. (_invoice.receipt.paid
  80. ? tr::lng_about_done()
  81. : tr::lng_cancel()),
  82. st::paymentsPanelButton)
  83. , _tipLightBg(TransparentColor(st::paymentsTipActive, kLightOpacity))
  84. , _tipLightRipple(
  85. TransparentColor(st::paymentsTipActive, kLightRippleOpacity))
  86. , _tipChosenBg(TransparentColor(st::paymentsTipActive, kChosenOpacity))
  87. , _tipChosenRipple(
  88. TransparentColor(st::paymentsTipActive, kChosenRippleOpacity))
  89. , _tipButton(TipButtonStyle(
  90. st::paymentsTipButton,
  91. _tipLightBg.color(),
  92. _tipLightRipple.color()))
  93. , _tipChosen(TipButtonStyle(
  94. st::paymentsTipChosen,
  95. _tipChosenBg.color(),
  96. _tipChosenRipple.color()))
  97. , _initialScrollTop(scrollTop) {
  98. setupControls();
  99. }
  100. rpl::producer<int> FormSummary::scrollTopValue() const {
  101. return _scroll->scrollTopValue();
  102. }
  103. bool FormSummary::showCriticalError(const TextWithEntities &text) {
  104. if (_invoice
  105. || (_scroll->height() - _layout->height()
  106. < st::paymentsPanelSize.height() / 2)) {
  107. return false;
  108. }
  109. Ui::AddSkip(_layout.get(), st::paymentsPricesTopSkip);
  110. _layout->add(object_ptr<FlatLabel>(
  111. _layout.get(),
  112. rpl::single(text),
  113. st::paymentsCriticalError));
  114. return true;
  115. }
  116. int FormSummary::contentHeight() const {
  117. return _invoice ? _scroll->height() : _layout->height();
  118. }
  119. void FormSummary::updateThumbnail(const QImage &thumbnail) {
  120. _invoice.cover.thumbnail = thumbnail;
  121. _thumbnails.fire_copy(thumbnail);
  122. }
  123. QString FormSummary::formatAmount(
  124. int64 amount,
  125. bool forceStripDotZero) const {
  126. return FillAmountAndCurrency(
  127. amount,
  128. _invoice.currency,
  129. forceStripDotZero);
  130. }
  131. int64 FormSummary::computeTotalAmount() const {
  132. const auto total = ranges::accumulate(
  133. _invoice.prices,
  134. int64(0),
  135. std::plus<>(),
  136. &LabeledPrice::price);
  137. const auto selected = ranges::find(
  138. _options.list,
  139. _options.selectedId,
  140. &ShippingOption::id);
  141. const auto shipping = (selected != end(_options.list))
  142. ? ranges::accumulate(
  143. selected->prices,
  144. int64(0),
  145. std::plus<>(),
  146. &LabeledPrice::price)
  147. : int64(0);
  148. return total + shipping + _invoice.tipsSelected;
  149. }
  150. void FormSummary::setupControls() {
  151. setupContent(_layout.get());
  152. if (_submit) {
  153. _submit->setTextTransform(
  154. Ui::RoundButton::TextTransform::NoTransform);
  155. _submit->addClickHandler([=] {
  156. _delegate->panelSubmit();
  157. });
  158. }
  159. _cancel->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
  160. _cancel->addClickHandler([=] {
  161. _delegate->panelRequestClose();
  162. });
  163. if (!_invoice) {
  164. if (_submit) {
  165. _submit->hide();
  166. }
  167. _cancel->hide();
  168. }
  169. using namespace rpl::mappers;
  170. _topShadow->toggleOn(
  171. _scroll->scrollTopValue() | rpl::map(_1 > 0));
  172. _bottomShadow->toggleOn(rpl::combine(
  173. _scroll->scrollTopValue(),
  174. _scroll->heightValue(),
  175. _layout->heightValue(),
  176. _1 + _2 < _3));
  177. rpl::merge(
  178. (_submit ? _submit->widthValue() : rpl::single(0)),
  179. _cancel->widthValue()
  180. ) | rpl::skip(2) | rpl::start_with_next([=] {
  181. updateControlsGeometry();
  182. }, lifetime());
  183. }
  184. void FormSummary::setupCover(not_null<VerticalLayout*> layout) {
  185. struct State {
  186. QImage thumbnail;
  187. FlatLabel *title = nullptr;
  188. FlatLabel *description = nullptr;
  189. FlatLabel *seller = nullptr;
  190. };
  191. const auto cover = layout->add(object_ptr<RpWidget>(layout));
  192. const auto state = cover->lifetime().make_state<State>();
  193. state->title = CreateChild<FlatLabel>(
  194. cover,
  195. _invoice.cover.title,
  196. st::paymentsTitle);
  197. state->description = CreateChild<FlatLabel>(
  198. cover,
  199. rpl::single(_invoice.cover.description),
  200. st::paymentsDescription);
  201. state->seller = CreateChild<FlatLabel>(
  202. cover,
  203. _invoice.cover.seller,
  204. st::paymentsSeller);
  205. cover->paintRequest(
  206. ) | rpl::start_with_next([=](QRect clip) {
  207. if (state->thumbnail.isNull()) {
  208. return;
  209. }
  210. const auto &padding = st::paymentsCoverPadding;
  211. const auto left = padding.left();
  212. const auto top = padding.top();
  213. const auto rect = QRect(
  214. QPoint(left, top),
  215. state->thumbnail.size() / state->thumbnail.devicePixelRatio());
  216. if (rect.intersects(clip)) {
  217. QPainter(cover).drawImage(rect, state->thumbnail);
  218. }
  219. }, cover->lifetime());
  220. rpl::combine(
  221. cover->widthValue(),
  222. _thumbnails.events_starting_with_copy(_invoice.cover.thumbnail)
  223. ) | rpl::start_with_next([=](int width, QImage &&thumbnail) {
  224. const auto &padding = st::paymentsCoverPadding;
  225. const auto thumbnailSkip = st::paymentsThumbnailSize.width()
  226. + st::paymentsThumbnailSkip;
  227. const auto left = padding.left()
  228. + (thumbnail.isNull() ? 0 : thumbnailSkip);
  229. const auto available = width
  230. - padding.left()
  231. - padding.right()
  232. - (thumbnail.isNull() ? 0 : thumbnailSkip);
  233. state->title->resizeToNaturalWidth(available);
  234. state->title->moveToLeft(
  235. left,
  236. padding.top() + st::paymentsTitleTop);
  237. state->description->resizeToNaturalWidth(available);
  238. state->description->moveToLeft(
  239. left,
  240. (state->title->y()
  241. + state->title->height()
  242. + st::paymentsDescriptionTop));
  243. state->seller->resizeToNaturalWidth(available);
  244. state->seller->moveToLeft(
  245. left,
  246. (state->description->y()
  247. + state->description->height()
  248. + st::paymentsSellerTop));
  249. const auto thumbnailHeight = padding.top()
  250. + (thumbnail.isNull()
  251. ? 0
  252. : int(thumbnail.height() / thumbnail.devicePixelRatio()))
  253. + padding.bottom();
  254. const auto height = state->seller->y()
  255. + state->seller->height()
  256. + padding.bottom();
  257. cover->resize(width, std::max(thumbnailHeight, height));
  258. state->thumbnail = std::move(thumbnail);
  259. cover->update();
  260. }, cover->lifetime());
  261. }
  262. void FormSummary::setupPrices(not_null<VerticalLayout*> layout) {
  263. const auto addRow = [&](
  264. const QString &label,
  265. const TextWithEntities &value,
  266. bool full = false) {
  267. const auto &st = full
  268. ? st::paymentsFullPriceAmount
  269. : st::paymentsPriceAmount;
  270. const auto right = CreateChild<FlatLabel>(
  271. layout.get(),
  272. rpl::single(value),
  273. st);
  274. const auto &padding = st::paymentsPricePadding;
  275. const auto left = layout->add(
  276. object_ptr<FlatLabel>(
  277. layout,
  278. label,
  279. (full
  280. ? st::paymentsFullPriceLabel
  281. : st::paymentsPriceLabel)),
  282. style::margins(
  283. padding.left(),
  284. padding.top(),
  285. (padding.right()
  286. + right->textMaxWidth()
  287. + 2 * st.style.font->spacew),
  288. padding.bottom()));
  289. rpl::combine(
  290. left->topValue(),
  291. layout->widthValue()
  292. ) | rpl::start_with_next([=](int top, int width) {
  293. right->moveToRight(st::paymentsPricePadding.right(), top, width);
  294. }, right->lifetime());
  295. return right;
  296. };
  297. Ui::AddSkip(layout, st::paymentsPricesTopSkip);
  298. if (_invoice.receipt) {
  299. addRow(
  300. tr::lng_payments_date_label(tr::now),
  301. { langDateTime(base::unixtime::parse(_invoice.receipt.date)) },
  302. true);
  303. Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
  304. Ui::AddDivider(layout);
  305. Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
  306. }
  307. const auto add = [&](
  308. const QString &label,
  309. int64 amount,
  310. bool full = false) {
  311. addRow(label, { formatAmount(amount) }, full);
  312. };
  313. for (const auto &price : _invoice.prices) {
  314. add(price.label, price.price);
  315. }
  316. const auto selected = ranges::find(
  317. _options.list,
  318. _options.selectedId,
  319. &ShippingOption::id);
  320. if (selected != end(_options.list)) {
  321. for (const auto &price : selected->prices) {
  322. add(price.label, price.price);
  323. }
  324. }
  325. const auto computedTotal = computeTotalAmount();
  326. const auto total = _invoice.receipt.paid
  327. ? _invoice.receipt.totalAmount
  328. : computedTotal;
  329. if (_invoice.receipt.paid) {
  330. if (const auto tips = total - computedTotal) {
  331. add(tr::lng_payments_tips_label(tr::now), tips);
  332. }
  333. } else if (_invoice.tipsMax > 0) {
  334. const auto text = formatAmount(_invoice.tipsSelected);
  335. const auto label = addRow(
  336. tr::lng_payments_tips_label(tr::now),
  337. Ui::Text::Link(text));
  338. label->overrideLinkClickHandler([=] {
  339. _delegate->panelChooseTips();
  340. });
  341. setupSuggestedTips(layout);
  342. }
  343. add(tr::lng_payments_total_label(tr::now), total, true);
  344. Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
  345. }
  346. void FormSummary::setupSuggestedTips(not_null<VerticalLayout*> layout) {
  347. if (_invoice.suggestedTips.empty()) {
  348. return;
  349. }
  350. struct Button {
  351. RoundButton *widget = nullptr;
  352. int minWidth = 0;
  353. };
  354. struct State {
  355. std::vector<Button> buttons;
  356. int maxWidth = 0;
  357. };
  358. const auto outer = layout->add(
  359. object_ptr<RpWidget>(layout),
  360. st::paymentsTipButtonsPadding);
  361. const auto state = outer->lifetime().make_state<State>();
  362. for (const auto amount : _invoice.suggestedTips) {
  363. const auto selected = (amount == _invoice.tipsSelected);
  364. const auto &st = selected
  365. ? _tipChosen
  366. : _tipButton;
  367. state->buttons.push_back(Button{
  368. .widget = CreateChild<RoundButton>(
  369. outer,
  370. rpl::single(formatAmount(amount, true)),
  371. st),
  372. });
  373. auto &button = state->buttons.back();
  374. button.widget->show();
  375. button.widget->setClickedCallback([=] {
  376. _delegate->panelChangeTips(selected ? 0 : amount);
  377. });
  378. button.minWidth = button.widget->width();
  379. state->maxWidth = std::max(state->maxWidth, button.minWidth);
  380. }
  381. outer->widthValue(
  382. ) | rpl::filter([=](int outerWidth) {
  383. return outerWidth >= state->maxWidth;
  384. }) | rpl::start_with_next([=](int outerWidth) {
  385. const auto skip = st::paymentsTipSkip;
  386. const auto &buttons = state->buttons;
  387. auto left = outerWidth;
  388. auto height = 0;
  389. auto rowStart = 0;
  390. auto rowEnd = 0;
  391. auto buttonWidths = std::vector<float64>();
  392. const auto layoutRow = [&] {
  393. const auto count = rowEnd - rowStart;
  394. if (!count) {
  395. return;
  396. }
  397. buttonWidths.resize(count);
  398. ranges::fill(buttonWidths, 0.);
  399. auto available = float64(outerWidth - (count - 1) * skip);
  400. auto zeros = count;
  401. do {
  402. const auto started = zeros;
  403. const auto average = available / zeros;
  404. for (auto i = 0; i != count; ++i) {
  405. if (buttonWidths[i] > 0.) {
  406. continue;
  407. }
  408. const auto min = buttons[rowStart + i].minWidth;
  409. if (min > average) {
  410. buttonWidths[i] = min;
  411. available -= min;
  412. --zeros;
  413. }
  414. }
  415. if (started == zeros) {
  416. for (auto i = 0; i != count; ++i) {
  417. if (!buttonWidths[i]) {
  418. buttonWidths[i] = average;
  419. }
  420. }
  421. break;
  422. }
  423. } while (zeros > 0);
  424. auto x = 0.;
  425. for (auto i = 0; i != count; ++i) {
  426. const auto button = buttons[rowStart + i].widget;
  427. auto right = x + buttonWidths[i];
  428. button->setFullWidth(
  429. int(base::SafeRound(right) - base::SafeRound(x)));
  430. button->moveToLeft(
  431. int(base::SafeRound(x)),
  432. height,
  433. outerWidth);
  434. x = right + skip;
  435. }
  436. height += buttons[0].widget->height() + skip;
  437. };
  438. for (const auto &button : buttons) {
  439. if (button.minWidth <= left) {
  440. left -= button.minWidth + skip;
  441. ++rowEnd;
  442. continue;
  443. }
  444. layoutRow();
  445. rowStart = rowEnd++;
  446. left = outerWidth - button.minWidth - skip;
  447. }
  448. layoutRow();
  449. outer->resize(outerWidth, height - skip);
  450. }, outer->lifetime());
  451. }
  452. void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
  453. Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
  454. const auto add = [&](
  455. rpl::producer<QString> title,
  456. const QString &label,
  457. const style::icon *icon,
  458. Fn<void()> handler) {
  459. const auto button = Settings::AddButtonWithLabel(
  460. layout,
  461. std::move(title),
  462. rpl::single(label),
  463. st::paymentsSectionButton,
  464. { .icon = icon });
  465. button->addClickHandler(std::move(handler));
  466. if (_invoice.receipt) {
  467. button->setAttribute(Qt::WA_TransparentForMouseEvents);
  468. }
  469. };
  470. add(
  471. tr::lng_payments_payment_method(),
  472. (_method.savedMethods.empty()
  473. ? QString()
  474. : _method.savedMethods[_method.savedMethodIndex].title),
  475. &st::paymentsIconPaymentMethod,
  476. [=] { _delegate->panelEditPaymentMethod(); });
  477. if (_invoice.isShippingAddressRequested) {
  478. auto list = QStringList();
  479. const auto push = [&](const QString &value) {
  480. if (!value.isEmpty()) {
  481. list.push_back(value);
  482. }
  483. };
  484. push(_information.shippingAddress.address1);
  485. push(_information.shippingAddress.address2);
  486. push(_information.shippingAddress.city);
  487. push(_information.shippingAddress.state);
  488. push(Countries::Instance().countryNameByISO2(
  489. _information.shippingAddress.countryIso2));
  490. push(_information.shippingAddress.postcode);
  491. add(
  492. tr::lng_payments_shipping_address(),
  493. list.join(", "),
  494. &st::paymentsIconShippingAddress,
  495. [=] { _delegate->panelEditShippingInformation(); });
  496. }
  497. if (!_options.list.empty()) {
  498. const auto selected = ranges::find(
  499. _options.list,
  500. _options.selectedId,
  501. &ShippingOption::id);
  502. add(
  503. tr::lng_payments_shipping_method(),
  504. (selected != end(_options.list)) ? selected->title : QString(),
  505. &st::paymentsIconShippingMethod,
  506. [=] { _delegate->panelChooseShippingOption(); });
  507. }
  508. if (_invoice.isNameRequested) {
  509. add(
  510. tr::lng_payments_info_name(),
  511. _information.name,
  512. &st::paymentsIconName,
  513. [=] { _delegate->panelEditName(); });
  514. }
  515. if (_invoice.isEmailRequested) {
  516. add(
  517. tr::lng_payments_info_email(),
  518. _information.email,
  519. &st::paymentsIconEmail,
  520. [=] { _delegate->panelEditEmail(); });
  521. }
  522. if (_invoice.isPhoneRequested) {
  523. add(
  524. tr::lng_payments_info_phone(),
  525. (_information.phone.isEmpty()
  526. ? QString()
  527. : Ui::FormatPhone(_information.phone)),
  528. &st::paymentsIconPhone,
  529. [=] { _delegate->panelEditPhone(); });
  530. }
  531. Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
  532. }
  533. void FormSummary::setupContent(not_null<VerticalLayout*> layout) {
  534. _scroll->widthValue(
  535. ) | rpl::start_with_next([=](int width) {
  536. layout->resizeToWidth(width);
  537. }, layout->lifetime());
  538. setupCover(layout);
  539. if (_invoice) {
  540. Ui::AddDivider(layout);
  541. setupPrices(layout);
  542. Ui::AddDivider(layout);
  543. setupSections(layout);
  544. }
  545. }
  546. void FormSummary::resizeEvent(QResizeEvent *e) {
  547. updateControlsGeometry();
  548. }
  549. void FormSummary::updateControlsGeometry() {
  550. const auto &padding = st::paymentsPanelPadding;
  551. const auto buttonsHeight = padding.top()
  552. + _cancel->height()
  553. + padding.bottom();
  554. const auto buttonsTop = height() - buttonsHeight;
  555. _scroll->setGeometry(0, 0, width(), buttonsTop);
  556. _topShadow->resizeToWidth(width());
  557. _topShadow->moveToLeft(0, 0);
  558. _bottomShadow->resizeToWidth(width());
  559. _bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
  560. auto right = padding.right();
  561. if (_submit) {
  562. _submit->moveToRight(right, buttonsTop + padding.top());
  563. right += _submit->width() + padding.left();
  564. }
  565. _cancel->moveToRight(right, buttonsTop + padding.top());
  566. _scroll->updateBars();
  567. if (buttonsTop > 0 && width() > 0) {
  568. if (const auto top = base::take(_initialScrollTop)) {
  569. _scroll->scrollToY(top);
  570. }
  571. }
  572. }
  573. } // namespace Payments::Ui