payments_edit_card.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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_edit_card.h"
  8. #include "payments/ui/payments_panel_delegate.h"
  9. #include "payments/ui/payments_field.h"
  10. #include "stripe/stripe_card_validator.h"
  11. #include "ui/widgets/scroll_area.h"
  12. #include "ui/widgets/buttons.h"
  13. #include "ui/widgets/labels.h"
  14. #include "ui/widgets/checkbox.h"
  15. #include "ui/wrap/vertical_layout.h"
  16. #include "ui/wrap/fade_wrap.h"
  17. #include "lang/lang_keys.h"
  18. #include "styles/style_payments.h"
  19. #include "styles/style_passport.h"
  20. #include <QtCore/QRegularExpression>
  21. namespace Payments::Ui {
  22. namespace {
  23. struct SimpleFieldState {
  24. QString value;
  25. int position = 0;
  26. };
  27. [[nodiscard]] uint32 ExtractYear(const QString &value) {
  28. return value.split('/').value(1).toInt() + 2000;
  29. }
  30. [[nodiscard]] uint32 ExtractMonth(const QString &value) {
  31. return value.split('/').value(0).toInt();
  32. }
  33. [[nodiscard]] QString RemoveNonNumbers(QString value) {
  34. static const auto RegExp = QRegularExpression("[^0-9]");
  35. return value.replace(RegExp, QString());
  36. }
  37. [[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) {
  38. return {
  39. .value = RemoveNonNumbers(state.value),
  40. .position = int(RemoveNonNumbers(
  41. state.value.mid(0, state.position)).size()),
  42. };
  43. }
  44. [[nodiscard]] SimpleFieldState PostprocessCardValidateResult(
  45. SimpleFieldState result) {
  46. const auto groups = Stripe::CardNumberFormat(result.value);
  47. auto position = 0;
  48. for (const auto length : groups) {
  49. position += length;
  50. if (position >= result.value.size()) {
  51. break;
  52. }
  53. result.value.insert(position, QChar(' '));
  54. if (result.position >= position) {
  55. ++result.position;
  56. }
  57. ++position;
  58. }
  59. return result;
  60. }
  61. [[nodiscard]] SimpleFieldState PostprocessExpireDateValidateResult(
  62. SimpleFieldState result) {
  63. if (result.value.isEmpty()) {
  64. return result;
  65. } else if (result.value[0] == '1'
  66. && (result.value.size() > 1)
  67. && result.value[1] > '2') {
  68. result.value = result.value.mid(0, 2);
  69. return result;
  70. } else if (result.value[0] > '1') {
  71. result.value = '0' + result.value;
  72. ++result.position;
  73. }
  74. if (result.value.size() > 1) {
  75. if (result.value.size() > 4) {
  76. result.value = result.value.mid(0, 4);
  77. }
  78. result.value.insert(2, '/');
  79. if (result.position >= 2) {
  80. ++result.position;
  81. }
  82. }
  83. return result;
  84. }
  85. [[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
  86. return (request.wasAnchor == request.wasPosition)
  87. && (request.wasPosition == request.nowPosition + 1)
  88. && (request.wasValue.mid(0, request.wasPosition - 1)
  89. == request.nowValue.mid(0, request.nowPosition))
  90. && (request.wasValue.mid(request.wasPosition)
  91. == request.nowValue.mid(request.nowPosition));
  92. }
  93. [[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
  94. return (request.wasAnchor == request.wasPosition)
  95. && (request.wasPosition == request.nowPosition)
  96. && (request.wasValue.mid(0, request.wasPosition)
  97. == request.nowValue.mid(0, request.nowPosition))
  98. && (request.wasValue.mid(request.wasPosition + 1)
  99. == request.nowValue.mid(request.nowPosition));
  100. }
  101. template <
  102. typename ValueValidator,
  103. typename ValueValidateResult = decltype(
  104. std::declval<ValueValidator>()(QString()))>
  105. [[nodiscard]] auto ComplexNumberValidator(
  106. ValueValidator valueValidator,
  107. Fn<SimpleFieldState(SimpleFieldState)> postprocess) {
  108. using namespace Stripe;
  109. return [=](FieldValidateRequest request) {
  110. const auto realNowState = [&] {
  111. const auto backspaced = IsBackspace(request);
  112. const auto deleted = IsDelete(request);
  113. if (!backspaced && !deleted) {
  114. return NumbersOnlyState({
  115. .value = request.nowValue,
  116. .position = request.nowPosition,
  117. });
  118. }
  119. const auto realWasState = NumbersOnlyState({
  120. .value = request.wasValue,
  121. .position = request.wasPosition,
  122. });
  123. const auto changedValue = deleted
  124. ? (realWasState.value.mid(0, realWasState.position)
  125. + realWasState.value.mid(realWasState.position + 1))
  126. : (realWasState.position > 1)
  127. ? (realWasState.value.mid(0, realWasState.position - 1)
  128. + realWasState.value.mid(realWasState.position))
  129. : realWasState.value.mid(realWasState.position);
  130. return SimpleFieldState{
  131. .value = changedValue,
  132. .position = (deleted
  133. ? realWasState.position
  134. : std::max(realWasState.position - 1, 0))
  135. };
  136. }();
  137. const auto result = valueValidator(realNowState.value);
  138. const auto postprocessed = postprocess(realNowState);
  139. return FieldValidateResult{
  140. .value = postprocessed.value,
  141. .position = postprocessed.position,
  142. .invalid = (result.state == ValidationState::Invalid),
  143. .finished = result.finished,
  144. };
  145. };
  146. }
  147. [[nodiscard]] auto CardNumberValidator() {
  148. return ComplexNumberValidator(
  149. Stripe::ValidateCard,
  150. PostprocessCardValidateResult);
  151. }
  152. [[nodiscard]] auto ExpireDateValidator(
  153. const std::optional<QDate> &overrideExpireDateThreshold) {
  154. return ComplexNumberValidator([=](const QString &date) {
  155. return Stripe::ValidateExpireDate(date, overrideExpireDateThreshold);
  156. }, PostprocessExpireDateValidateResult);
  157. }
  158. [[nodiscard]] auto CvcValidator(Fn<QString()> number) {
  159. using namespace Stripe;
  160. return [=](FieldValidateRequest request) {
  161. const auto realNowState = NumbersOnlyState({
  162. .value = request.nowValue,
  163. .position = request.nowPosition,
  164. });
  165. const auto result = ValidateCvc(number(), realNowState.value);
  166. return FieldValidateResult{
  167. .value = realNowState.value,
  168. .position = realNowState.position,
  169. .invalid = (result.state == ValidationState::Invalid),
  170. .finished = result.finished,
  171. };
  172. };
  173. }
  174. [[nodiscard]] auto CardHolderNameValidator() {
  175. return [=](FieldValidateRequest request) {
  176. return FieldValidateResult{
  177. .value = request.nowValue.toUpper(),
  178. .position = request.nowPosition,
  179. .invalid = request.nowValue.isEmpty(),
  180. };
  181. };
  182. }
  183. } // namespace
  184. EditCard::EditCard(
  185. QWidget *parent,
  186. const NativeMethodDetails &native,
  187. CardField field,
  188. not_null<PanelDelegate*> delegate)
  189. : _delegate(delegate)
  190. , _native(native)
  191. , _scroll(this, st::passportPanelScroll)
  192. , _topShadow(this)
  193. , _bottomShadow(this)
  194. , _submit(
  195. this,
  196. tr::lng_about_done(),
  197. st::paymentsPanelButton)
  198. , _cancel(
  199. this,
  200. tr::lng_cancel(),
  201. st::paymentsPanelButton) {
  202. setupControls();
  203. }
  204. void EditCard::setFocus(CardField field) {
  205. _focusField = field;
  206. if (const auto control = lookupField(field)) {
  207. _scroll->ensureWidgetVisible(control->widget());
  208. control->setFocus();
  209. }
  210. }
  211. void EditCard::setFocusFast(CardField field) {
  212. _focusField = field;
  213. if (const auto control = lookupField(field)) {
  214. _scroll->ensureWidgetVisible(control->widget());
  215. control->setFocusFast();
  216. }
  217. }
  218. void EditCard::showError(CardField field) {
  219. if (const auto control = lookupField(field)) {
  220. _scroll->ensureWidgetVisible(control->widget());
  221. control->showError();
  222. }
  223. }
  224. void EditCard::setupControls() {
  225. const auto inner = setupContent();
  226. _submit->addClickHandler([=] {
  227. _delegate->panelValidateCard(collect(), (_save && _save->checked()));
  228. });
  229. _cancel->addClickHandler([=] {
  230. _delegate->panelCancelEdit();
  231. });
  232. using namespace rpl::mappers;
  233. _topShadow->toggleOn(
  234. _scroll->scrollTopValue() | rpl::map(_1 > 0));
  235. _bottomShadow->toggleOn(rpl::combine(
  236. _scroll->scrollTopValue(),
  237. _scroll->heightValue(),
  238. inner->heightValue(),
  239. _1 + _2 < _3));
  240. }
  241. not_null<RpWidget*> EditCard::setupContent() {
  242. const auto inner = _scroll->setOwnedWidget(
  243. object_ptr<VerticalLayout>(this));
  244. _scroll->widthValue(
  245. ) | rpl::start_with_next([=](int width) {
  246. inner->resizeToWidth(width);
  247. }, inner->lifetime());
  248. const auto showBox = [=](object_ptr<BoxContent> box) {
  249. _delegate->panelShowBox(std::move(box));
  250. };
  251. auto last = (Field*)nullptr;
  252. const auto make = [&](QWidget *parent, FieldConfig &&config) {
  253. auto result = std::make_unique<Field>(parent, std::move(config));
  254. if (last) {
  255. last->setNextField(result.get());
  256. result->setPreviousField(last);
  257. }
  258. last = result.get();
  259. return result;
  260. };
  261. const auto add = [&](FieldConfig &&config) {
  262. auto result = make(inner, std::move(config));
  263. inner->add(result->ownedWidget(), st::paymentsFieldPadding);
  264. return result;
  265. };
  266. _number = add({
  267. .type = FieldType::CardNumber,
  268. .placeholder = tr::lng_payments_card_number(),
  269. .validator = CardNumberValidator(),
  270. });
  271. auto container = inner->add(
  272. object_ptr<FixedHeightWidget>(
  273. inner,
  274. _number->widget()->height()),
  275. st::paymentsFieldPadding);
  276. _expire = make(container, {
  277. .type = FieldType::CardExpireDate,
  278. .placeholder = tr::lng_payments_card_expire_date(),
  279. .validator = ExpireDateValidator(
  280. _delegate->panelOverrideExpireDateThreshold()),
  281. });
  282. _cvc = make(container, {
  283. .type = FieldType::CardCVC,
  284. .placeholder = tr::lng_payments_card_cvc(),
  285. .validator = CvcValidator([=] { return _number->value(); }),
  286. });
  287. container->widthValue(
  288. ) | rpl::start_with_next([=](int width) {
  289. const auto left = (width - st::paymentsExpireCvcSkip) / 2;
  290. const auto right = width - st::paymentsExpireCvcSkip - left;
  291. _expire->widget()->resizeToWidth(left);
  292. _cvc->widget()->resizeToWidth(right);
  293. _expire->widget()->moveToLeft(0, 0, width);
  294. _cvc->widget()->moveToRight(0, 0, width);
  295. }, container->lifetime());
  296. if (_native.needCardholderName) {
  297. _name = add({
  298. .type = FieldType::Text,
  299. .placeholder = tr::lng_payments_card_holder(),
  300. .validator = CardHolderNameValidator(),
  301. });
  302. }
  303. if (_native.needCountry || _native.needZip) {
  304. inner->add(
  305. object_ptr<Ui::FlatLabel>(
  306. inner,
  307. tr::lng_payments_billing_address(),
  308. st::paymentsBillingInformationTitle),
  309. st::paymentsBillingInformationTitlePadding);
  310. }
  311. if (_native.needCountry) {
  312. _country = add({
  313. .type = FieldType::Country,
  314. .placeholder = tr::lng_payments_billing_country(),
  315. .validator = RequiredFinishedValidator(),
  316. .showBox = showBox,
  317. .defaultCountry = _native.defaultCountry,
  318. });
  319. }
  320. if (_native.needZip) {
  321. _zip = add({
  322. .type = FieldType::Text,
  323. .placeholder = tr::lng_payments_billing_zip_code(),
  324. .validator = RequiredValidator(),
  325. });
  326. if (_country) {
  327. _country->finished(
  328. ) | rpl::start_with_next([=] {
  329. _zip->setFocus();
  330. }, lifetime());
  331. }
  332. }
  333. if (_native.canSaveInformation) {
  334. _save = inner->add(
  335. object_ptr<Ui::Checkbox>(
  336. inner,
  337. tr::lng_payments_save_information(tr::now),
  338. false),
  339. st::paymentsSaveCheckboxPadding);
  340. }
  341. last->submitted(
  342. ) | rpl::start_with_next([=] {
  343. _delegate->panelValidateCard(collect(), _save && _save->checked());
  344. }, lifetime());
  345. return inner;
  346. }
  347. void EditCard::resizeEvent(QResizeEvent *e) {
  348. updateControlsGeometry();
  349. }
  350. void EditCard::focusInEvent(QFocusEvent *e) {
  351. if (const auto control = lookupField(_focusField)) {
  352. control->setFocusFast();
  353. }
  354. }
  355. void EditCard::updateControlsGeometry() {
  356. const auto &padding = st::paymentsPanelPadding;
  357. const auto buttonsHeight = padding.top()
  358. + _cancel->height()
  359. + padding.bottom();
  360. const auto buttonsTop = height() - buttonsHeight;
  361. _scroll->setGeometry(0, 0, width(), buttonsTop);
  362. _topShadow->resizeToWidth(width());
  363. _topShadow->moveToLeft(0, 0);
  364. _bottomShadow->resizeToWidth(width());
  365. _bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
  366. auto right = padding.right();
  367. _submit->moveToRight(right, buttonsTop + padding.top());
  368. right += _submit->width() + padding.left();
  369. _cancel->moveToRight(right, buttonsTop + padding.top());
  370. _scroll->updateBars();
  371. }
  372. auto EditCard::lookupField(CardField field) const -> Field* {
  373. switch (field) {
  374. case CardField::Number: return _number.get();
  375. case CardField::Cvc: return _cvc.get();
  376. case CardField::ExpireDate: return _expire.get();
  377. case CardField::Name: return _name.get();
  378. case CardField::AddressCountry: return _country.get();
  379. case CardField::AddressZip: return _zip.get();
  380. }
  381. Unexpected("Unknown field in EditCard::controlForField.");
  382. }
  383. UncheckedCardDetails EditCard::collect() const {
  384. return {
  385. .number = _number ? _number->value() : QString(),
  386. .cvc = _cvc ? _cvc->value() : QString(),
  387. .expireYear = _expire ? ExtractYear(_expire->value()) : 0,
  388. .expireMonth = _expire ? ExtractMonth(_expire->value()) : 0,
  389. .cardholderName = _name ? _name->value() : QString(),
  390. .addressCountry = _country ? _country->value() : QString(),
  391. .addressZip = _zip ? _zip->value() : QString(),
  392. };
  393. }
  394. } // namespace Payments::Ui