init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
/*
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
*/
using "ui/basic.style";
using "info/info.style";
paymentsPanelSize: size(392px, 600px);
paymentsPanelButton: RoundButton(defaultBoxButton) {
width: -36px;
height: 36px;
style: TextStyle(defaultTextStyle) {
font: boxButtonFont;
}
}
paymentsPanelSubmit: RoundButton(defaultActiveButton) {
width: -36px;
height: 36px;
style: TextStyle(defaultTextStyle) {
font: boxButtonFont;
}
}
paymentsPanelPadding: margins(8px, 12px, 15px, 12px);
paymentsCoverPadding: margins(26px, 0px, 26px, 13px);
paymentsDescription: FlatLabel(defaultFlatLabel) {
minWidth: 160px;
textFg: windowFg;
}
paymentsTitle: FlatLabel(paymentsDescription) {
style: semiboldTextStyle;
}
paymentsSeller: FlatLabel(paymentsDescription) {
textFg: windowSubTextFg;
}
paymentsWebviewBottom: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
paymentsPriceLabel: paymentsDescription;
paymentsPriceAmount: defaultFlatLabel;
paymentsFullPriceLabel: paymentsTitle;
paymentsFullPriceAmount: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
}
paymentsTitleTop: 0px;
paymentsDescriptionTop: 3px;
paymentsSellerTop: 4px;
paymentsThumbnailSize: size(80px, 80px);
paymentsThumbnailSkip: 18px;
paymentsPricesTopSkip: 12px;
paymentsPricesBottomSkip: 13px;
paymentsPricePadding: margins(28px, 6px, 28px, 5px);
paymentsTipSkip: 8px;
paymentsTipButton: RoundButton(defaultLightButton) {
textFg: paymentsTipActive;
textFgOver: paymentsTipActive;
textBgOver: transparent;
width: -16px;
height: 28px;
textTop: 5px;
}
paymentsTipChosen: RoundButton(paymentsTipButton) {
textFg: windowFgActive;
textFgOver: windowFgActive;
textBgOver: transparent;
}
paymentsTipButtonsPadding: margins(26px, 6px, 26px, 6px);
paymentsSectionsTopSkip: 11px;
paymentsSectionButton: SettingsButton(infoProfileButton) {
padding: margins(68px, 11px, 14px, 9px);
}
paymentsIconPaymentMethod: icon {{ "payments/payment_card", windowBoldFg }};
paymentsIconShippingAddress: icon {{ "payments/payment_address", windowBoldFg }};
paymentsIconName: icon {{ "payments/payment_name", windowBoldFg }};
paymentsIconEmail: icon {{ "payments/payment_email", windowBoldFg }};
paymentsIconPhone: icon {{ "payments/payment_phone", windowBoldFg }};
paymentsIconShippingMethod: icon {{ "payments/payment_shipping", windowBoldFg }};
paymentsField: defaultInputField;
paymentsMoneyField: InputField(paymentsField) {
textMargins: margins(0px, 4px, 0px, 4px);
heightMin: 30px;
}
paymentsFieldAdditional: FlatLabel(defaultFlatLabel) {
style: boxTextStyle;
}
paymentsFieldPadding: margins(28px, 0px, 28px, 2px);
paymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px);
paymentsExpireCvcSkip: 34px;
paymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: windowActiveTextFg;
minWidth: 240px;
}
paymentsBillingInformationTitlePadding: margins(28px, 26px, 28px, 1px);
paymentsShippingMargin: margins(27px, 11px, 27px, 20px);
paymentsShippingLabel: FlatLabel(defaultFlatLabel) {
style: boxTextStyle;
}
paymentsShippingPrice: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
paymentsShippingLabelPosition: point(43px, 8px);
paymentsShippingPricePosition: point(43px, 29px);
paymentTipsErrorLabel: FlatLabel(defaultFlatLabel) {
minWidth: 275px;
textFg: boxTextFgError;
}
paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px);
paymentsToProviderLabel: paymentsShippingPrice;
paymentsToProviderPadding: margins(28px, 6px, 28px, 6px);
paymentsCriticalError: FlatLabel(boxLabel) {
minWidth: 340px;
align: align(top);
textFg: windowSubTextFg;
}
paymentsCriticalErrorPadding: margins(10px, 40px, 10px, 0px);
paymentsLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
size: size(24px, 24px);
color: windowSubTextFg;
thickness: 4px;
}
botWebViewPanelSize: size(384px, 694px);
botWebViewBottomPadding: margins(12px, 12px, 12px, 12px);
botWebViewBottomSkip: point(12px, 8px);
botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
height: 40px;
style: TextStyle(defaultTextStyle) {
font: boxButtonFont;
}
textTop: 11px;
}
botWebViewRadialStroke: 3px;
botWebViewMenu: PopupMenu(popupMenuWithIcons) {
maxHeight: 360px;
}

View File

@@ -0,0 +1,437 @@
/*
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 "payments/ui/payments_edit_card.h"
#include "payments/ui/payments_panel_delegate.h"
#include "payments/ui/payments_field.h"
#include "stripe/stripe_card_validator.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "lang/lang_keys.h"
#include "styles/style_payments.h"
#include "styles/style_passport.h"
#include <QtCore/QRegularExpression>
namespace Payments::Ui {
namespace {
struct SimpleFieldState {
QString value;
int position = 0;
};
[[nodiscard]] uint32 ExtractYear(const QString &value) {
return value.split('/').value(1).toInt() + 2000;
}
[[nodiscard]] uint32 ExtractMonth(const QString &value) {
return value.split('/').value(0).toInt();
}
[[nodiscard]] QString RemoveNonNumbers(QString value) {
static const auto RegExp = QRegularExpression("[^0-9]");
return value.replace(RegExp, QString());
}
[[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) {
return {
.value = RemoveNonNumbers(state.value),
.position = int(RemoveNonNumbers(
state.value.mid(0, state.position)).size()),
};
}
[[nodiscard]] SimpleFieldState PostprocessCardValidateResult(
SimpleFieldState result) {
const auto groups = Stripe::CardNumberFormat(result.value);
auto position = 0;
for (const auto length : groups) {
position += length;
if (position >= result.value.size()) {
break;
}
result.value.insert(position, QChar(' '));
if (result.position >= position) {
++result.position;
}
++position;
}
return result;
}
[[nodiscard]] SimpleFieldState PostprocessExpireDateValidateResult(
SimpleFieldState result) {
if (result.value.isEmpty()) {
return result;
} else if (result.value[0] == '1'
&& (result.value.size() > 1)
&& result.value[1] > '2') {
result.value = result.value.mid(0, 2);
return result;
} else if (result.value[0] > '1') {
result.value = '0' + result.value;
++result.position;
}
if (result.value.size() > 1) {
if (result.value.size() > 4) {
result.value = result.value.mid(0, 4);
}
result.value.insert(2, '/');
if (result.position >= 2) {
++result.position;
}
}
return result;
}
[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition + 1)
&& (request.wasValue.mid(0, request.wasPosition - 1)
== request.nowValue.mid(0, request.nowPosition))
&& (request.wasValue.mid(request.wasPosition)
== request.nowValue.mid(request.nowPosition));
}
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition)
&& (request.wasValue.mid(0, request.wasPosition)
== request.nowValue.mid(0, request.nowPosition))
&& (request.wasValue.mid(request.wasPosition + 1)
== request.nowValue.mid(request.nowPosition));
}
template <
typename ValueValidator,
typename ValueValidateResult = decltype(
std::declval<ValueValidator>()(QString()))>
[[nodiscard]] auto ComplexNumberValidator(
ValueValidator valueValidator,
Fn<SimpleFieldState(SimpleFieldState)> postprocess) {
using namespace Stripe;
return [=](FieldValidateRequest request) {
const auto realNowState = [&] {
const auto backspaced = IsBackspace(request);
const auto deleted = IsDelete(request);
if (!backspaced && !deleted) {
return NumbersOnlyState({
.value = request.nowValue,
.position = request.nowPosition,
});
}
const auto realWasState = NumbersOnlyState({
.value = request.wasValue,
.position = request.wasPosition,
});
const auto changedValue = deleted
? (realWasState.value.mid(0, realWasState.position)
+ realWasState.value.mid(realWasState.position + 1))
: (realWasState.position > 1)
? (realWasState.value.mid(0, realWasState.position - 1)
+ realWasState.value.mid(realWasState.position))
: realWasState.value.mid(realWasState.position);
return SimpleFieldState{
.value = changedValue,
.position = (deleted
? realWasState.position
: std::max(realWasState.position - 1, 0))
};
}();
const auto result = valueValidator(realNowState.value);
const auto postprocessed = postprocess(realNowState);
return FieldValidateResult{
.value = postprocessed.value,
.position = postprocessed.position,
.invalid = (result.state == ValidationState::Invalid),
.finished = result.finished,
};
};
}
[[nodiscard]] auto CardNumberValidator() {
return ComplexNumberValidator(
Stripe::ValidateCard,
PostprocessCardValidateResult);
}
[[nodiscard]] auto ExpireDateValidator(
const std::optional<QDate> &overrideExpireDateThreshold) {
return ComplexNumberValidator([=](const QString &date) {
return Stripe::ValidateExpireDate(date, overrideExpireDateThreshold);
}, PostprocessExpireDateValidateResult);
}
[[nodiscard]] auto CvcValidator(Fn<QString()> number) {
using namespace Stripe;
return [=](FieldValidateRequest request) {
const auto realNowState = NumbersOnlyState({
.value = request.nowValue,
.position = request.nowPosition,
});
const auto result = ValidateCvc(number(), realNowState.value);
return FieldValidateResult{
.value = realNowState.value,
.position = realNowState.position,
.invalid = (result.state == ValidationState::Invalid),
.finished = result.finished,
};
};
}
[[nodiscard]] auto CardHolderNameValidator() {
return [=](FieldValidateRequest request) {
return FieldValidateResult{
.value = request.nowValue.toUpper(),
.position = request.nowPosition,
.invalid = request.nowValue.isEmpty(),
};
};
}
} // namespace
EditCard::EditCard(
QWidget *parent,
const NativeMethodDetails &native,
CardField field,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _native(native)
, _scroll(this, st::passportPanelScroll)
, _topShadow(this)
, _bottomShadow(this)
, _submit(
this,
tr::lng_about_done(),
st::paymentsPanelButton)
, _cancel(
this,
tr::lng_cancel(),
st::paymentsPanelButton) {
setupControls();
}
void EditCard::setFocus(CardField field) {
_focusField = field;
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->setFocus();
}
}
void EditCard::setFocusFast(CardField field) {
_focusField = field;
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->setFocusFast();
}
}
void EditCard::showError(CardField field) {
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->showError();
}
}
void EditCard::setupControls() {
const auto inner = setupContent();
_submit->addClickHandler([=] {
_delegate->panelValidateCard(collect(), (_save && _save->checked()));
});
_cancel->addClickHandler([=] {
_delegate->panelCancelEdit();
});
using namespace rpl::mappers;
_topShadow->toggleOn(
_scroll->scrollTopValue() | rpl::map(_1 > 0));
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
inner->heightValue(),
_1 + _2 < _3));
}
not_null<RpWidget*> EditCard::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::on_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
const auto showBox = [=](object_ptr<BoxContent> box) {
_delegate->panelShowBox(std::move(box));
};
auto last = (Field*)nullptr;
const auto make = [&](QWidget *parent, FieldConfig &&config) {
auto result = std::make_unique<Field>(parent, std::move(config));
if (last) {
last->setNextField(result.get());
result->setPreviousField(last);
}
last = result.get();
return result;
};
const auto add = [&](FieldConfig &&config) {
auto result = make(inner, std::move(config));
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
return result;
};
_number = add({
.type = FieldType::CardNumber,
.placeholder = tr::lng_payments_card_number(),
.validator = CardNumberValidator(),
});
auto container = inner->add(
object_ptr<FixedHeightWidget>(
inner,
_number->widget()->height()),
st::paymentsFieldPadding);
_expire = make(container, {
.type = FieldType::CardExpireDate,
.placeholder = tr::lng_payments_card_expire_date(),
.validator = ExpireDateValidator(
_delegate->panelOverrideExpireDateThreshold()),
});
_cvc = make(container, {
.type = FieldType::CardCVC,
.placeholder = tr::lng_payments_card_cvc(),
.validator = CvcValidator([=] { return _number->value(); }),
});
container->widthValue(
) | rpl::on_next([=](int width) {
const auto left = (width - st::paymentsExpireCvcSkip) / 2;
const auto right = width - st::paymentsExpireCvcSkip - left;
_expire->widget()->resizeToWidth(left);
_cvc->widget()->resizeToWidth(right);
_expire->widget()->moveToLeft(0, 0, width);
_cvc->widget()->moveToRight(0, 0, width);
}, container->lifetime());
if (_native.needCardholderName) {
_name = add({
.type = FieldType::Text,
.placeholder = tr::lng_payments_card_holder(),
.validator = CardHolderNameValidator(),
});
}
if (_native.needCountry || _native.needZip) {
inner->add(
object_ptr<Ui::FlatLabel>(
inner,
tr::lng_payments_billing_address(),
st::paymentsBillingInformationTitle),
st::paymentsBillingInformationTitlePadding);
}
if (_native.needCountry) {
_country = add({
.type = FieldType::Country,
.placeholder = tr::lng_payments_billing_country(),
.validator = RequiredFinishedValidator(),
.showBox = showBox,
.defaultCountry = _native.defaultCountry,
});
}
if (_native.needZip) {
_zip = add({
.type = FieldType::Text,
.placeholder = tr::lng_payments_billing_zip_code(),
.validator = RequiredValidator(),
});
if (_country) {
_country->finished(
) | rpl::on_next([=] {
_zip->setFocus();
}, lifetime());
}
}
if (_native.canSaveInformation) {
_save = inner->add(
object_ptr<Ui::Checkbox>(
inner,
tr::lng_payments_save_information(tr::now),
false),
st::paymentsSaveCheckboxPadding);
}
last->submitted(
) | rpl::on_next([=] {
_delegate->panelValidateCard(collect(), _save && _save->checked());
}, lifetime());
return inner;
}
void EditCard::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void EditCard::focusInEvent(QFocusEvent *e) {
if (const auto control = lookupField(_focusField)) {
control->setFocusFast();
}
}
void EditCard::updateControlsGeometry() {
const auto &padding = st::paymentsPanelPadding;
const auto buttonsHeight = padding.top()
+ _cancel->height()
+ padding.bottom();
const auto buttonsTop = height() - buttonsHeight;
_scroll->setGeometry(0, 0, width(), buttonsTop);
_topShadow->resizeToWidth(width());
_topShadow->moveToLeft(0, 0);
_bottomShadow->resizeToWidth(width());
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
auto right = padding.right();
_submit->moveToRight(right, buttonsTop + padding.top());
right += _submit->width() + padding.left();
_cancel->moveToRight(right, buttonsTop + padding.top());
_scroll->updateBars();
}
auto EditCard::lookupField(CardField field) const -> Field* {
switch (field) {
case CardField::Number: return _number.get();
case CardField::Cvc: return _cvc.get();
case CardField::ExpireDate: return _expire.get();
case CardField::Name: return _name.get();
case CardField::AddressCountry: return _country.get();
case CardField::AddressZip: return _zip.get();
}
Unexpected("Unknown field in EditCard::controlForField.");
}
UncheckedCardDetails EditCard::collect() const {
return {
.number = _number ? _number->value() : QString(),
.cvc = _cvc ? _cvc->value() : QString(),
.expireYear = _expire ? ExtractYear(_expire->value()) : 0,
.expireMonth = _expire ? ExtractMonth(_expire->value()) : 0,
.cardholderName = _name ? _name->value() : QString(),
.addressCountry = _country ? _country->value() : QString(),
.addressZip = _zip ? _zip->value() : QString(),
};
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,72 @@
/*
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
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class Checkbox;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class Field;
class EditCard final : public RpWidget {
public:
EditCard(
QWidget *parent,
const NativeMethodDetails &native,
CardField field,
not_null<PanelDelegate*> delegate);
void setFocus(CardField field);
void setFocusFast(CardField field);
void showError(CardField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(CardField field) const;
[[nodiscard]] UncheckedCardDetails collect() const;
const not_null<PanelDelegate*> _delegate;
NativeMethodDetails _native;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
std::unique_ptr<Field> _number;
std::unique_ptr<Field> _cvc;
std::unique_ptr<Field> _expire;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _zip;
Checkbox *_save = nullptr;
CardField _focusField = CardField::Number;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,282 @@
/*
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 "payments/ui/payments_edit_information.h"
#include "payments/ui/payments_panel_delegate.h"
#include "payments/ui/payments_field.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "lang/lang_keys.h"
#include "styles/style_payments.h"
#include "styles/style_passport.h"
namespace Payments::Ui {
namespace {
constexpr auto kMaxStreetSize = 64;
constexpr auto kMaxPostcodeSize = 10;
constexpr auto kMaxNameSize = 64;
constexpr auto kMaxEmailSize = 128;
constexpr auto kMaxPhoneSize = 16;
constexpr auto kMinCitySize = 2;
constexpr auto kMaxCitySize = 64;
} // namespace
EditInformation::EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
InformationField field,
not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _invoice(invoice)
, _information(current)
, _scroll(this, st::passportPanelScroll)
, _topShadow(this)
, _bottomShadow(this)
, _submit(
this,
tr::lng_settings_save(),
st::paymentsPanelButton)
, _cancel(
this,
tr::lng_cancel(),
st::paymentsPanelButton) {
setupControls();
}
EditInformation::~EditInformation() = default;
void EditInformation::setFocus(InformationField field) {
_focusField = field;
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->setFocus();
}
}
void EditInformation::setFocusFast(InformationField field) {
_focusField = field;
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->setFocusFast();
}
}
void EditInformation::showError(InformationField field) {
if (const auto control = lookupField(field)) {
_scroll->ensureWidgetVisible(control->widget());
control->showError();
}
}
void EditInformation::setupControls() {
const auto inner = setupContent();
_submit->addClickHandler([=] {
_delegate->panelValidateInformation(collect());
});
_cancel->addClickHandler([=] {
_delegate->panelCancelEdit();
});
using namespace rpl::mappers;
_topShadow->toggleOn(
_scroll->scrollTopValue() | rpl::map(_1 > 0));
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
inner->heightValue(),
_1 + _2 < _3));
}
not_null<RpWidget*> EditInformation::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
_scroll->widthValue(
) | rpl::on_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
const auto showBox = [=](object_ptr<BoxContent> box) {
_delegate->panelShowBox(std::move(box));
};
auto last = (Field*)nullptr;
const auto add = [&](FieldConfig &&config) {
auto result = std::make_unique<Field>(inner, std::move(config));
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
if (last) {
last->setNextField(result.get());
result->setPreviousField(last);
}
last = result.get();
return result;
};
if (_invoice.isShippingAddressRequested) {
_street1 = add({
.placeholder = tr::lng_payments_address_street1(),
.value = _information.shippingAddress.address1,
.validator = RangeLengthValidator(1, kMaxStreetSize),
});
_street2 = add({
.placeholder = tr::lng_payments_address_street2(),
.value = _information.shippingAddress.address2,
.validator = MaxLengthValidator(kMaxStreetSize),
});
_city = add({
.placeholder = tr::lng_payments_address_city(),
.value = _information.shippingAddress.city,
.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),
});
_state = add({
.placeholder = tr::lng_payments_address_state(),
.value = _information.shippingAddress.state,
});
_country = add({
.type = FieldType::Country,
.placeholder = tr::lng_payments_address_country(),
.value = _information.shippingAddress.countryIso2,
.validator = RequiredFinishedValidator(),
.showBox = showBox,
.defaultCountry = _information.defaultCountry,
});
_postcode = add({
.placeholder = tr::lng_payments_address_postcode(),
.value = _information.shippingAddress.postcode,
.validator = RangeLengthValidator(1, kMaxPostcodeSize),
});
}
if (_invoice.isNameRequested) {
_name = add({
.placeholder = tr::lng_payments_info_name(),
.value = _information.name,
.validator = RangeLengthValidator(1, kMaxNameSize),
});
}
if (_invoice.isEmailRequested) {
_email = add({
.type = FieldType::Email,
.placeholder = tr::lng_payments_info_email(),
.value = _information.email,
.validator = RangeLengthValidator(1, kMaxEmailSize),
});
}
if (_invoice.isPhoneRequested) {
_phone = add({
.type = FieldType::Phone,
.placeholder = tr::lng_payments_info_phone(),
.value = _information.phone,
.validator = RangeLengthValidator(1, kMaxPhoneSize),
.defaultPhone = _information.defaultPhone,
});
}
const auto emailToProvider = _invoice.isEmailRequested
&& _invoice.emailSentToProvider;
const auto phoneToProvider = _invoice.isPhoneRequested
&& _invoice.phoneSentToProvider;
if (emailToProvider || phoneToProvider) {
inner->add(
object_ptr<Ui::FlatLabel>(
inner,
((emailToProvider && phoneToProvider)
? tr::lng_payments_to_provider_phone_email
: emailToProvider
? tr::lng_payments_to_provider_email
: tr::lng_payments_to_provider_phone)(
lt_provider,
rpl::single(_invoice.provider)),
st::paymentsToProviderLabel),
st::paymentsToProviderPadding);
}
_save = inner->add(
object_ptr<Ui::Checkbox>(
inner,
tr::lng_payments_save_information(tr::now),
true),
st::paymentsSaveCheckboxPadding);
if (last) {
last->submitted(
) | rpl::on_next([=] {
_delegate->panelValidateInformation(collect());
}, lifetime());
}
return inner;
}
void EditInformation::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void EditInformation::focusInEvent(QFocusEvent *e) {
if (const auto control = lookupField(_focusField)) {
control->setFocus();
}
}
void EditInformation::updateControlsGeometry() {
const auto &padding = st::paymentsPanelPadding;
const auto buttonsHeight = padding.top()
+ _cancel->height()
+ padding.bottom();
const auto buttonsTop = height() - buttonsHeight;
_scroll->setGeometry(0, 0, width(), buttonsTop);
_topShadow->resizeToWidth(width());
_topShadow->moveToLeft(0, 0);
_bottomShadow->resizeToWidth(width());
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
auto right = padding.right();
_submit->moveToRight(right, buttonsTop + padding.top());
right += _submit->width() + padding.left();
_cancel->moveToRight(right, buttonsTop + padding.top());
_scroll->updateBars();
}
auto EditInformation::lookupField(InformationField field) const -> Field* {
switch (field) {
case InformationField::ShippingStreet: return _street1.get();
case InformationField::ShippingCity: return _city.get();
case InformationField::ShippingState: return _state.get();
case InformationField::ShippingCountry: return _country.get();
case InformationField::ShippingPostcode: return _postcode.get();
case InformationField::Name: return _name.get();
case InformationField::Email: return _email.get();
case InformationField::Phone: return _phone.get();
}
Unexpected("Unknown field in EditInformation::lookupField.");
}
RequestedInformation EditInformation::collect() const {
return {
.defaultPhone = _information.defaultPhone,
.defaultCountry = _information.defaultCountry,
.save = _save->checked(),
.name = _name ? _name->value() : QString(),
.phone = _phone ? _phone->value() : QString(),
.email = _email ? _email->value() : QString(),
.shippingAddress = {
.address1 = _street1 ? _street1->value() : QString(),
.address2 = _street2 ? _street2->value() : QString(),
.city = _city ? _city->value() : QString(),
.state = _state ? _state->value() : QString(),
.countryIso2 = _country ? _country->value() : QString(),
.postcode = _postcode ? _postcode->value() : QString(),
},
};
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,80 @@
/*
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
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class InputField;
class MaskedInputField;
class Checkbox;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class Field;
class EditInformation final : public RpWidget {
public:
EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
InformationField field,
not_null<PanelDelegate*> delegate);
~EditInformation();
void setFocus(InformationField field);
void setFocusFast(InformationField field);
void showError(InformationField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(InformationField field) const;
[[nodiscard]] RequestedInformation collect() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
std::unique_ptr<Field> _street1;
std::unique_ptr<Field> _street2;
std::unique_ptr<Field> _city;
std::unique_ptr<Field> _state;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _postcode;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _email;
std::unique_ptr<Field> _phone;
Checkbox *_save = nullptr;
InformationField _focusField = InformationField::ShippingStreet;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,719 @@
/*
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 "payments/ui/payments_field.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/boxes/country_select_box.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "ui/widgets/fields/special_fields.h"
#include "countries/countries_instance.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "base/qt/qt_common_adapters.h"
#include "styles/style_payments.h"
#include <QtCore/QRegularExpression>
#include <QtWidgets/QTextEdit>
namespace Payments::Ui {
namespace {
struct SimpleFieldState {
QString value;
int position = 0;
};
[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {
return (rule.thousands == '.' || rule.thousands == ',')
? ' '
: rule.thousands;
}
[[nodiscard]] QString RemoveNonNumbers(QString value) {
static const auto RegExp = QRegularExpression("[^0-9]");
return value.replace(RegExp, QString());
}
[[nodiscard]] SimpleFieldState CleanMoneyState(
const CurrencyRule &rule,
SimpleFieldState state) {
const auto withDecimal = state.value.replace(
QChar('.'),
rule.decimal
).replace(
QChar(','),
rule.decimal
);
const auto digitsLimit = 16 - rule.exponent;
const auto beforePosition = state.value.mid(0, state.position);
auto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));
if (decimalPosition < 0) {
state = {
.value = RemoveNonNumbers(state.value),
.position = int(RemoveNonNumbers(beforePosition).size()),
};
} else {
const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
state.value.mid(0, decimalPosition));
state = {
.value = (onlyNumbersBeforeDecimal
+ QChar(rule.decimal)
+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
.position = int(RemoveNonNumbers(beforePosition).size()
+ (state.position > decimalPosition ? 1 : 0)),
};
decimalPosition = onlyNumbersBeforeDecimal.size();
const auto maxLength = decimalPosition + 1 + rule.exponent;
if (state.value.size() > maxLength) {
state = {
.value = state.value.mid(0, maxLength),
.position = std::min(state.position, maxLength),
};
}
}
if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {
state = {
.value = QChar('0') + state.value,
.position = state.position + 1,
};
if (decimalPosition >= 0) {
++decimalPosition;
}
}
auto skip = 0;
while (state.value.size() > skip + 1
&& state.value[skip] == QChar('0')
&& state.value[skip + 1] != QChar(rule.decimal)) {
++skip;
}
state = {
.value = state.value.mid(skip),
.position = std::max(state.position - skip, 0),
};
if (decimalPosition >= 0) {
Assert(decimalPosition >= skip);
decimalPosition -= skip;
if (decimalPosition > digitsLimit) {
state = {
.value = (state.value.mid(0, digitsLimit)
+ state.value.mid(decimalPosition)),
.position = (state.position > digitsLimit
? std::max(
state.position - (decimalPosition - digitsLimit),
digitsLimit)
: state.position),
};
}
} else if (state.value.size() > digitsLimit) {
state = {
.value = state.value.mid(0, digitsLimit),
.position = std::min(state.position, digitsLimit),
};
}
return state;
}
[[nodiscard]] SimpleFieldState PostprocessMoneyResult(
const CurrencyRule &rule,
SimpleFieldState result) {
const auto position = result.value.indexOf(rule.decimal);
const auto from = (position >= 0) ? position : result.value.size();
for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {
result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));
if (result.position >= insertAt) {
++result.position;
}
}
return result;
}
[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition + 1)
&& (base::StringViewMid(request.wasValue, 0, request.wasPosition - 1)
== base::StringViewMid(request.nowValue, 0, request.nowPosition))
&& (base::StringViewMid(request.wasValue, request.wasPosition)
== base::StringViewMid(request.nowValue, request.nowPosition));
}
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition)
&& (base::StringViewMid(request.wasValue, 0, request.wasPosition)
== base::StringViewMid(request.nowValue, 0, request.nowPosition))
&& (base::StringViewMid(request.wasValue, request.wasPosition + 1)
== base::StringViewMid(request.nowValue, request.nowPosition));
}
[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {
return [=](FieldValidateRequest request) {
const auto realNowState = [&] {
const auto backspaced = IsBackspace(request);
const auto deleted = IsDelete(request);
if (!backspaced && !deleted) {
return CleanMoneyState(rule, {
.value = request.nowValue,
.position = request.nowPosition,
});
}
const auto realWasState = CleanMoneyState(rule, {
.value = request.wasValue,
.position = request.wasPosition,
});
const auto changedValue = deleted
? (realWasState.value.mid(0, realWasState.position)
+ realWasState.value.mid(realWasState.position + 1))
: (realWasState.position > 1)
? (realWasState.value.mid(0, realWasState.position - 1)
+ realWasState.value.mid(realWasState.position))
: realWasState.value.mid(realWasState.position);
return SimpleFieldState{
.value = changedValue,
.position = (deleted
? realWasState.position
: std::max(realWasState.position - 1, 0))
};
}();
const auto postprocessed = PostprocessMoneyResult(
rule,
realNowState);
return FieldValidateResult{
.value = postprocessed.value,
.position = postprocessed.position,
};
};
}
[[nodiscard]] QString Parse(const FieldConfig &config) {
if (config.type == FieldType::Country) {
return Countries::Instance().countryNameByISO2(config.value);
} else if (config.type == FieldType::Money) {
const auto amount = config.value.toLongLong();
if (!amount) {
return QString();
}
const auto rule = LookupCurrencyRule(config.currency);
const auto value = std::abs(amount) / std::pow(10., rule.exponent);
const auto precision = (!rule.stripDotZero
|| std::floor(value) != value)
? rule.exponent
: 0;
return FormatWithSeparators(
value,
precision,
rule.decimal,
FieldThousandsSeparator(rule));
}
return config.value;
}
[[nodiscard]] QString Format(
const FieldConfig &config,
const QString &parsed,
const QString &countryIso2) {
if (config.type == FieldType::Country) {
return countryIso2;
} else if (config.type == FieldType::Money) {
static const auto RegExp = QRegularExpression("[^0-9\\.]");
const auto rule = LookupCurrencyRule(config.currency);
const auto real = QString(parsed).replace(
QChar(rule.decimal),
QChar('.')
).replace(
QChar(','),
QChar('.')
).replace(
RegExp,
QString()
).toDouble();
return QString::number(
int64(base::SafeRound(real * std::pow(10., rule.exponent))));
} else if (config.type == FieldType::CardNumber
|| config.type == FieldType::CardCVC) {
static const auto RegExp = QRegularExpression("[^0-9]");
return QString(parsed).replace(RegExp, QString());
}
return parsed;
}
[[nodiscard]] bool UseMaskedField(FieldType type) {
switch (type) {
case FieldType::Text:
case FieldType::Email:
return false;
case FieldType::CardNumber:
case FieldType::CardExpireDate:
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::Phone:
case FieldType::Money:
return true;
}
Unexpected("FieldType in Payments::Ui::UseMaskedField.");
}
[[nodiscard]] base::unique_qptr<RpWidget> CreateWrap(
QWidget *parent,
FieldConfig &config) {
switch (config.type) {
case FieldType::Text:
case FieldType::Email:
return base::make_unique_q<InputField>(
parent,
st::paymentsField,
std::move(config.placeholder),
Parse(config));
case FieldType::CardNumber:
case FieldType::CardExpireDate:
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::Phone:
case FieldType::Money:
return base::make_unique_q<RpWidget>(parent);
}
Unexpected("FieldType in Payments::Ui::CreateWrap.");
}
[[nodiscard]] InputField *LookupInputField(
not_null<RpWidget*> wrap,
FieldConfig &config) {
return UseMaskedField(config.type)
? nullptr
: static_cast<InputField*>(wrap.get());
}
[[nodiscard]] MaskedInputField *CreateMoneyField(
not_null<RpWidget*> wrap,
FieldConfig &config,
rpl::producer<> textPossiblyChanged) {
struct State {
CurrencyRule rule;
style::InputField st;
QString currencyText;
int currencySkip = 0;
FlatLabel *left = nullptr;
FlatLabel *right = nullptr;
};
const auto state = wrap->lifetime().make_state<State>(State{
.rule = LookupCurrencyRule(config.currency),
.st = st::paymentsMoneyField,
});
const auto &rule = state->rule;
state->currencySkip = rule.space ? state->st.style.font->spacew : 0;
state->currencyText = ((!rule.left && rule.space)
? QString(QChar(' '))
: QString()) + (*rule.international
? QString(rule.international)
: config.currency) + ((rule.left && rule.space)
? QString(QChar(' '))
: QString());
if (rule.left) {
state->left = CreateChild<FlatLabel>(
wrap.get(),
state->currencyText,
st::paymentsFieldAdditional);
}
state->right = CreateChild<FlatLabel>(
wrap.get(),
QString(),
st::paymentsFieldAdditional);
const auto leftSkip = state->left
? (state->left->textMaxWidth() + state->currencySkip)
: 0;
const auto rightSkip = st::paymentsFieldAdditional.style.font->width(
QString(QChar(rule.decimal))
+ QString(QChar('0')).repeated(rule.exponent)
+ (rule.left ? QString() : state->currencyText));
state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);
state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);
const auto result = CreateChild<MaskedInputField>(
wrap.get(),
state->st,
std::move(config.placeholder),
Parse(config));
result->setPlaceholderHidden(true);
if (state->left) {
state->left->move(0, state->st.textMargins.top());
}
const auto updateRight = [=] {
const auto text = result->getLastText();
const auto width = state->st.style.font->width(text);
const auto &rule = state->rule;
const auto symbol = QChar(rule.decimal);
const auto decimal = text.indexOf(symbol);
const auto zeros = (decimal >= 0)
? std::max(rule.exponent - int(text.size() - decimal - 1), 0)
: rule.stripDotZero
? 0
: rule.exponent;
const auto valueDecimalSeparator = (decimal >= 0 || !zeros)
? QString()
: QString(symbol);
const auto zeroString = QString(QChar('0'));
const auto valueRightPart = (text.isEmpty() ? zeroString : QString())
+ valueDecimalSeparator
+ zeroString.repeated(zeros);
const auto right = valueRightPart
+ (rule.left ? QString() : state->currencyText);
state->right->setText(right);
state->right->setTextColorOverride(valueRightPart.isEmpty()
? std::nullopt
: std::make_optional(st::windowSubTextFg->c));
state->right->move(
(state->st.textMargins.left()
+ width
+ ((rule.left || !valueRightPart.isEmpty())
? 0
: state->currencySkip)),
state->st.textMargins.top());
};
std::move(
textPossiblyChanged
) | rpl::on_next(updateRight, result->lifetime());
if (state->left) {
state->left->raise();
}
state->right->raise();
return result;
}
[[nodiscard]] MaskedInputField *LookupMaskedField(
not_null<RpWidget*> wrap,
FieldConfig &config,
rpl::producer<> textPossiblyChanged) {
if (!UseMaskedField(config.type)) {
return nullptr;
}
switch (config.type) {
case FieldType::Text:
case FieldType::Email:
return nullptr;
case FieldType::CardNumber:
case FieldType::CardExpireDate:
case FieldType::CardCVC:
case FieldType::Country:
return CreateChild<MaskedInputField>(
wrap.get(),
st::paymentsField,
std::move(config.placeholder),
Parse(config));
case FieldType::Phone:
return CreateChild<PhoneInput>(
wrap.get(),
st::paymentsField,
std::move(config.placeholder),
Countries::ExtractPhoneCode(config.defaultPhone),
Parse(config),
[](const QString &s) { return Countries::Groups(s); });
case FieldType::Money:
return CreateMoneyField(
wrap,
config,
std::move(textPossiblyChanged));
}
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
}
} // namespace
Field::Field(QWidget *parent, FieldConfig &&config)
: _config(config)
, _wrap(CreateWrap(parent, config))
, _input(LookupInputField(_wrap.get(), config))
, _masked(LookupMaskedField(
_wrap.get(),
config,
_textPossiblyChanged.events_starting_with({})))
, _countryIso2(config.value) {
if (_masked) {
setupMaskedGeometry();
}
if (_config.type == FieldType::Country) {
setupCountry();
}
if (const auto &validator = config.validator) {
setupValidator(validator);
} else if (config.type == FieldType::Money) {
setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
}
setupFrontBackspace();
setupSubmit();
}
RpWidget *Field::widget() const {
return _wrap.get();
}
object_ptr<RpWidget> Field::ownedWidget() const {
return object_ptr<RpWidget>::fromRaw(_wrap.get());
}
QString Field::value() const {
return Format(
_config,
_input ? _input->getLastText() : _masked->getLastText(),
_countryIso2);
}
rpl::producer<> Field::frontBackspace() const {
return _frontBackspace.events();
}
rpl::producer<> Field::finished() const {
return _finished.events();
}
rpl::producer<> Field::submitted() const {
return _submitted.events();
}
void Field::setupMaskedGeometry() {
Expects(_masked != nullptr);
_wrap->resize(_masked->size());
_wrap->widthValue(
) | rpl::on_next([=](int width) {
_masked->resize(width, _masked->height());
}, _masked->lifetime());
_masked->heightValue(
) | rpl::on_next([=](int height) {
_wrap->resize(_wrap->width(), height);
}, _masked->lifetime());
}
void Field::setupCountry() {
Expects(_config.type == FieldType::Country);
Expects(_masked != nullptr);
QObject::connect(_masked, &MaskedInputField::focused, [=] {
setFocus();
const auto name = Countries::Instance().countryNameByISO2(
_countryIso2);
const auto country = !name.isEmpty()
? _countryIso2
: !_config.defaultCountry.isEmpty()
? _config.defaultCountry
: Platform::SystemCountry();
auto box = Box<CountrySelectBox>(
country,
CountrySelectBox::Type::Countries);
const auto raw = box.data();
raw->countryChosen(
) | rpl::on_next([=](QString iso2) {
_countryIso2 = iso2;
_masked->setText(Countries::Instance().countryNameByISO2(iso2));
_masked->hideError();
raw->closeBox();
if (!iso2.isEmpty()) {
if (_nextField) {
_nextField->activate();
} else {
_submitted.fire({});
}
}
}, _masked->lifetime());
raw->boxClosing() | rpl::on_next([=] {
setFocus();
}, _masked->lifetime());
_config.showBox(std::move(box));
});
}
void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
Expects(validator != nullptr);
const auto state = [=]() -> State {
if (_masked) {
const auto position = _masked->cursorPosition();
const auto selectionStart = _masked->selectionStart();
const auto selectionEnd = _masked->selectionEnd();
return {
.value = _masked->getLastText(),
.position = position,
.anchor = (selectionStart == selectionEnd
? position
: (selectionStart == position)
? selectionEnd
: selectionStart),
};
}
const auto cursor = _input->textCursor();
return {
.value = _input->getLastText(),
.position = cursor.position(),
.anchor = cursor.anchor(),
};
};
const auto save = [=] {
_was = state();
};
const auto setText = [=](const QString &text) {
if (_masked) {
_masked->setText(text);
} else {
_input->setText(text);
}
};
const auto setPosition = [=](int position) {
if (_masked) {
_masked->setCursorPosition(position);
} else {
auto cursor = _input->textCursor();
cursor.setPosition(position);
_input->setTextCursor(cursor);
}
};
const auto validate = [=] {
if (_validating) {
return;
}
_validating = true;
const auto guard = gsl::finally([&] {
_validating = false;
save();
_textPossiblyChanged.fire({});
});
const auto now = state();
const auto result = validator(ValidateRequest{
.wasValue = _was.value,
.wasPosition = _was.position,
.wasAnchor = _was.anchor,
.nowValue = now.value,
.nowPosition = now.position,
});
_valid = result.finished || !result.invalid;
const auto changed = (result.value != now.value);
if (changed) {
setText(result.value);
}
if (changed || result.position != now.position) {
setPosition(result.position);
}
if (result.finished) {
_finished.fire({});
} else if (result.invalid) {
Ui::PostponeCall(
_masked ? (QWidget*)_masked : _input,
[=] { showErrorNoFocus(); });
}
};
if (_masked) {
QObject::connect(_masked, &QLineEdit::cursorPositionChanged, save);
QObject::connect(_masked, &MaskedInputField::changed, validate);
} else {
const auto raw = _input->rawTextEdit();
QObject::connect(raw, &QTextEdit::cursorPositionChanged, save);
_input->changes(
) | rpl::on_next(validate, _input->lifetime());
}
}
void Field::setupFrontBackspace() {
const auto filter = [=](not_null<QEvent*> e) {
const auto frontBackspace = (e->type() == QEvent::KeyPress)
&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)
&& (_masked
? (_masked->cursorPosition() == 0
&& _masked->selectionLength() == 0)
: (_input->textCursor().position() == 0
&& _input->textCursor().anchor() == 0));
if (frontBackspace) {
_frontBackspace.fire({});
}
return base::EventFilterResult::Continue;
};
if (_masked) {
base::install_event_filter(_masked, filter);
} else {
base::install_event_filter(_input->rawTextEdit(), filter);
}
}
void Field::setupSubmit() {
const auto submitted = [=] {
if (!_valid) {
showError();
} else if (_nextField) {
_nextField->activate();
} else {
_submitted.fire({});
}
};
if (_masked) {
QObject::connect(_masked, &MaskedInputField::submitted, submitted);
} else {
_input->submits(
) | rpl::on_next(submitted, _input->lifetime());
}
}
void Field::setNextField(not_null<Field*> field) {
_nextField = field;
finished() | rpl::on_next([=] {
field->setFocus();
}, _masked ? _masked->lifetime() : _input->lifetime());
}
void Field::setPreviousField(not_null<Field*> field) {
frontBackspace(
) | rpl::on_next([=] {
field->setFocus();
}, _masked ? _masked->lifetime() : _input->lifetime());
}
void Field::activate() {
if (_input) {
_input->setFocus();
} else {
_masked->setFocus();
}
}
void Field::setFocus() {
if (_config.type == FieldType::Country) {
_wrap->setFocus();
} else {
activate();
}
}
void Field::setFocusFast() {
if (_config.type == FieldType::Country) {
setFocus();
} else if (_input) {
_input->setFocusFast();
} else {
_masked->setFocusFast();
}
}
void Field::showError() {
if (_config.type == FieldType::Country) {
setFocus();
_masked->showErrorNoFocus();
} else if (_input) {
_input->showError();
} else {
_masked->showError();
}
}
void Field::showErrorNoFocus() {
if (_input) {
_input->showErrorNoFocus();
} else {
_masked->showErrorNoFocus();
}
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,143 @@
/*
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
*/
#pragma once
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
namespace Ui {
class RpWidget;
class InputField;
class MaskedInputField;
class BoxContent;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
enum class FieldType {
Text,
CardNumber,
CardExpireDate,
CardCVC,
Country,
Phone,
Email,
Money,
};
struct FieldValidateRequest {
QString wasValue;
int wasPosition = 0;
int wasAnchor = 0;
QString nowValue;
int nowPosition = 0;
};
struct FieldValidateResult {
QString value;
int position = 0;
bool invalid = false;
bool finished = false;
};
[[nodiscard]] inline auto RangeLengthValidator(int minLength, int maxLength) {
return [=](FieldValidateRequest request) {
return FieldValidateResult{
.value = request.nowValue,
.position = request.nowPosition,
.invalid = (request.nowValue.size() < minLength
|| request.nowValue.size() > maxLength),
};
};
}
[[nodiscard]] inline auto MaxLengthValidator(int maxLength) {
return RangeLengthValidator(0, maxLength);
}
[[nodiscard]] inline auto RequiredValidator() {
return RangeLengthValidator(1, std::numeric_limits<int>::max());
}
[[nodiscard]] inline auto RequiredFinishedValidator() {
return [=](FieldValidateRequest request) {
return FieldValidateResult{
.value = request.nowValue,
.position = request.nowPosition,
.invalid = request.nowValue.isEmpty(),
.finished = !request.nowValue.isEmpty(),
};
};
}
struct FieldConfig {
FieldType type = FieldType::Text;
rpl::producer<QString> placeholder;
QString value;
Fn<FieldValidateResult(FieldValidateRequest)> validator;
Fn<void(object_ptr<BoxContent>)> showBox;
QString currency;
QString defaultPhone;
QString defaultCountry;
};
class Field final {
public:
Field(QWidget *parent, FieldConfig &&config);
[[nodiscard]] RpWidget *widget() const;
[[nodiscard]] object_ptr<RpWidget> ownedWidget() const;
[[nodiscard]] QString value() const;
[[nodiscard]] rpl::producer<> frontBackspace() const;
[[nodiscard]] rpl::producer<> finished() const;
[[nodiscard]] rpl::producer<> submitted() const;
void activate();
void setFocus();
void setFocusFast();
void showError();
void showErrorNoFocus();
void setNextField(not_null<Field*> field);
void setPreviousField(not_null<Field*> field);
private:
struct State {
QString value;
int position = 0;
int anchor = 0;
};
using ValidateRequest = FieldValidateRequest;
using ValidateResult = FieldValidateResult;
void setupMaskedGeometry();
void setupCountry();
void setupValidator(Fn<ValidateResult(ValidateRequest)> validator);
void setupFrontBackspace();
void setupSubmit();
const FieldConfig _config;
const base::unique_qptr<RpWidget> _wrap;
rpl::event_stream<> _frontBackspace;
rpl::event_stream<> _finished;
rpl::event_stream<> _submitted;
rpl::event_stream<> _textPossiblyChanged; // Must be above _masked.
InputField *_input = nullptr;
MaskedInputField *_masked = nullptr;
Field *_nextField = nullptr;
QString _countryIso2;
State _was;
bool _validating = false;
bool _valid = true;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,612 @@
/*
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 "payments/ui/payments_form_summary.h"
#include "payments/ui/payments_panel_delegate.h"
#include "settings/settings_common.h" // AddButtonWithLabel.
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "countries/countries_instance.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "styles/style_payments.h"
#include "styles/style_passport.h"
namespace Payments::Ui {
namespace {
constexpr auto kLightOpacity = 0.1;
constexpr auto kLightRippleOpacity = 0.11;
constexpr auto kChosenOpacity = 0.8;
constexpr auto kChosenRippleOpacity = 0.5;
[[nodiscard]] Fn<QColor()> TransparentColor(
const style::color &c,
float64 opacity) {
return [&c, opacity] {
return QColor(
c->c.red(),
c->c.green(),
c->c.blue(),
c->c.alpha() * opacity);
};
}
[[nodiscard]] style::RoundButton TipButtonStyle(
const style::RoundButton &original,
const style::color &light,
const style::color &ripple) {
auto result = original;
result.textBg = light;
result.ripple.color = ripple;
return result;
}
} // namespace
using namespace ::Ui;
class PanelDelegate;
FormSummary::FormSummary(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate,
int scrollTop)
: _delegate(delegate)
, _invoice(invoice)
, _method(method)
, _options(options)
, _information(current)
, _scroll(this, st::passportPanelScroll)
, _layout(_scroll->setOwnedWidget(object_ptr<VerticalLayout>(this)))
, _topShadow(this)
, _bottomShadow(this)
, _submit(_invoice.receipt.paid
? object_ptr<RoundButton>(nullptr)
: object_ptr<RoundButton>(
this,
tr::lng_payments_pay_amount(
lt_amount,
rpl::single(formatAmount(computeTotalAmount()))),
st::paymentsPanelSubmit))
, _cancel(
this,
(_invoice.receipt.paid
? tr::lng_about_done()
: tr::lng_cancel()),
st::paymentsPanelButton)
, _tipLightBg(TransparentColor(st::paymentsTipActive, kLightOpacity))
, _tipLightRipple(
TransparentColor(st::paymentsTipActive, kLightRippleOpacity))
, _tipChosenBg(TransparentColor(st::paymentsTipActive, kChosenOpacity))
, _tipChosenRipple(
TransparentColor(st::paymentsTipActive, kChosenRippleOpacity))
, _tipButton(TipButtonStyle(
st::paymentsTipButton,
_tipLightBg.color(),
_tipLightRipple.color()))
, _tipChosen(TipButtonStyle(
st::paymentsTipChosen,
_tipChosenBg.color(),
_tipChosenRipple.color()))
, _initialScrollTop(scrollTop) {
setupControls();
}
rpl::producer<int> FormSummary::scrollTopValue() const {
return _scroll->scrollTopValue();
}
bool FormSummary::showCriticalError(const TextWithEntities &text) {
if (_invoice
|| (_scroll->height() - _layout->height()
< st::paymentsPanelSize.height() / 2)) {
return false;
}
Ui::AddSkip(_layout.get(), st::paymentsPricesTopSkip);
_layout->add(
object_ptr<FlatLabel>(
_layout.get(),
rpl::single(text),
st::paymentsCriticalError),
style::al_top);
return true;
}
int FormSummary::contentHeight() const {
return _invoice ? _scroll->height() : _layout->height();
}
void FormSummary::updateThumbnail(const QImage &thumbnail) {
_invoice.cover.thumbnail = thumbnail;
_thumbnails.fire_copy(thumbnail);
}
QString FormSummary::formatAmount(
int64 amount,
bool forceStripDotZero) const {
return FillAmountAndCurrency(
amount,
_invoice.currency,
forceStripDotZero);
}
int64 FormSummary::computeTotalAmount() const {
const auto total = ranges::accumulate(
_invoice.prices,
int64(0),
std::plus<>(),
&LabeledPrice::price);
const auto selected = ranges::find(
_options.list,
_options.selectedId,
&ShippingOption::id);
const auto shipping = (selected != end(_options.list))
? ranges::accumulate(
selected->prices,
int64(0),
std::plus<>(),
&LabeledPrice::price)
: int64(0);
return total + shipping + _invoice.tipsSelected;
}
void FormSummary::setupControls() {
setupContent(_layout.get());
if (_submit) {
_submit->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
_submit->addClickHandler([=] {
_delegate->panelSubmit();
});
}
_cancel->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
_cancel->addClickHandler([=] {
_delegate->panelRequestClose();
});
if (!_invoice) {
if (_submit) {
_submit->hide();
}
_cancel->hide();
}
using namespace rpl::mappers;
_topShadow->toggleOn(
_scroll->scrollTopValue() | rpl::map(_1 > 0));
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
_layout->heightValue(),
_1 + _2 < _3));
rpl::merge(
(_submit ? _submit->widthValue() : rpl::single(0)),
_cancel->widthValue()
) | rpl::skip(2) | rpl::on_next([=] {
updateControlsGeometry();
}, lifetime());
}
void FormSummary::setupCover(not_null<VerticalLayout*> layout) {
struct State {
QImage thumbnail;
FlatLabel *title = nullptr;
FlatLabel *description = nullptr;
FlatLabel *seller = nullptr;
};
const auto cover = layout->add(object_ptr<RpWidget>(layout));
const auto state = cover->lifetime().make_state<State>();
state->title = CreateChild<FlatLabel>(
cover,
_invoice.cover.title,
st::paymentsTitle);
state->description = CreateChild<FlatLabel>(
cover,
rpl::single(_invoice.cover.description),
st::paymentsDescription);
state->seller = CreateChild<FlatLabel>(
cover,
_invoice.cover.seller,
st::paymentsSeller);
cover->paintRequest(
) | rpl::on_next([=](QRect clip) {
if (state->thumbnail.isNull()) {
return;
}
const auto &padding = st::paymentsCoverPadding;
const auto left = padding.left();
const auto top = padding.top();
const auto rect = QRect(
QPoint(left, top),
state->thumbnail.size() / state->thumbnail.devicePixelRatio());
if (rect.intersects(clip)) {
QPainter(cover).drawImage(rect, state->thumbnail);
}
}, cover->lifetime());
rpl::combine(
cover->widthValue(),
_thumbnails.events_starting_with_copy(_invoice.cover.thumbnail)
) | rpl::on_next([=](int width, QImage &&thumbnail) {
const auto &padding = st::paymentsCoverPadding;
const auto thumbnailSkip = st::paymentsThumbnailSize.width()
+ st::paymentsThumbnailSkip;
const auto left = padding.left()
+ (thumbnail.isNull() ? 0 : thumbnailSkip);
const auto available = width
- padding.left()
- padding.right()
- (thumbnail.isNull() ? 0 : thumbnailSkip);
state->title->resizeToNaturalWidth(available);
state->title->moveToLeft(
left,
padding.top() + st::paymentsTitleTop);
state->description->resizeToNaturalWidth(available);
state->description->moveToLeft(
left,
(state->title->y()
+ state->title->height()
+ st::paymentsDescriptionTop));
state->seller->resizeToNaturalWidth(available);
state->seller->moveToLeft(
left,
(state->description->y()
+ state->description->height()
+ st::paymentsSellerTop));
const auto thumbnailHeight = padding.top()
+ (thumbnail.isNull()
? 0
: int(thumbnail.height() / thumbnail.devicePixelRatio()))
+ padding.bottom();
const auto height = state->seller->y()
+ state->seller->height()
+ padding.bottom();
cover->resize(width, std::max(thumbnailHeight, height));
state->thumbnail = std::move(thumbnail);
cover->update();
}, cover->lifetime());
}
void FormSummary::setupPrices(not_null<VerticalLayout*> layout) {
const auto addRow = [&](
const QString &label,
const TextWithEntities &value,
bool full = false) {
const auto &st = full
? st::paymentsFullPriceAmount
: st::paymentsPriceAmount;
const auto right = CreateChild<FlatLabel>(
layout.get(),
rpl::single(value),
st);
const auto &padding = st::paymentsPricePadding;
const auto left = layout->add(
object_ptr<FlatLabel>(
layout,
label,
(full
? st::paymentsFullPriceLabel
: st::paymentsPriceLabel)),
style::margins(
padding.left(),
padding.top(),
(padding.right()
+ right->textMaxWidth()
+ 2 * st.style.font->spacew),
padding.bottom()));
rpl::combine(
left->topValue(),
layout->widthValue()
) | rpl::on_next([=](int top, int width) {
right->moveToRight(st::paymentsPricePadding.right(), top, width);
}, right->lifetime());
return right;
};
Ui::AddSkip(layout, st::paymentsPricesTopSkip);
if (_invoice.receipt) {
addRow(
tr::lng_payments_date_label(tr::now),
{ langDateTime(base::unixtime::parse(_invoice.receipt.date)) },
true);
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
Ui::AddDivider(layout);
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
}
const auto add = [&](
const QString &label,
int64 amount,
bool full = false) {
addRow(label, { formatAmount(amount) }, full);
};
for (const auto &price : _invoice.prices) {
add(price.label, price.price);
}
const auto selected = ranges::find(
_options.list,
_options.selectedId,
&ShippingOption::id);
if (selected != end(_options.list)) {
for (const auto &price : selected->prices) {
add(price.label, price.price);
}
}
const auto computedTotal = computeTotalAmount();
const auto total = _invoice.receipt.paid
? _invoice.receipt.totalAmount
: computedTotal;
if (_invoice.receipt.paid) {
if (const auto tips = total - computedTotal) {
add(tr::lng_payments_tips_label(tr::now), tips);
}
} else if (_invoice.tipsMax > 0) {
const auto text = formatAmount(_invoice.tipsSelected);
const auto label = addRow(
tr::lng_payments_tips_label(tr::now),
tr::link(text));
label->overrideLinkClickHandler([=] {
_delegate->panelChooseTips();
});
setupSuggestedTips(layout);
}
add(tr::lng_payments_total_label(tr::now), total, true);
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
}
void FormSummary::setupSuggestedTips(not_null<VerticalLayout*> layout) {
if (_invoice.suggestedTips.empty()) {
return;
}
struct Button {
RoundButton *widget = nullptr;
int minWidth = 0;
};
struct State {
std::vector<Button> buttons;
int maxWidth = 0;
};
const auto outer = layout->add(
object_ptr<RpWidget>(layout),
st::paymentsTipButtonsPadding);
const auto state = outer->lifetime().make_state<State>();
for (const auto amount : _invoice.suggestedTips) {
const auto selected = (amount == _invoice.tipsSelected);
const auto &st = selected
? _tipChosen
: _tipButton;
state->buttons.push_back(Button{
.widget = CreateChild<RoundButton>(
outer,
rpl::single(formatAmount(amount, true)),
st),
});
auto &button = state->buttons.back();
button.widget->show();
button.widget->setClickedCallback([=] {
_delegate->panelChangeTips(selected ? 0 : amount);
});
button.minWidth = button.widget->width();
state->maxWidth = std::max(state->maxWidth, button.minWidth);
}
outer->widthValue(
) | rpl::filter([=](int outerWidth) {
return outerWidth >= state->maxWidth;
}) | rpl::on_next([=](int outerWidth) {
const auto skip = st::paymentsTipSkip;
const auto &buttons = state->buttons;
auto left = outerWidth;
auto height = 0;
auto rowStart = 0;
auto rowEnd = 0;
auto buttonWidths = std::vector<float64>();
const auto layoutRow = [&] {
const auto count = rowEnd - rowStart;
if (!count) {
return;
}
buttonWidths.resize(count);
ranges::fill(buttonWidths, 0.);
auto available = float64(outerWidth - (count - 1) * skip);
auto zeros = count;
do {
const auto started = zeros;
const auto average = available / zeros;
for (auto i = 0; i != count; ++i) {
if (buttonWidths[i] > 0.) {
continue;
}
const auto min = buttons[rowStart + i].minWidth;
if (min > average) {
buttonWidths[i] = min;
available -= min;
--zeros;
}
}
if (started == zeros) {
for (auto i = 0; i != count; ++i) {
if (!buttonWidths[i]) {
buttonWidths[i] = average;
}
}
break;
}
} while (zeros > 0);
auto x = 0.;
for (auto i = 0; i != count; ++i) {
const auto button = buttons[rowStart + i].widget;
auto right = x + buttonWidths[i];
button->setFullWidth(
int(base::SafeRound(right) - base::SafeRound(x)));
button->moveToLeft(
int(base::SafeRound(x)),
height,
outerWidth);
x = right + skip;
}
height += buttons[0].widget->height() + skip;
};
for (const auto &button : buttons) {
if (button.minWidth <= left) {
left -= button.minWidth + skip;
++rowEnd;
continue;
}
layoutRow();
rowStart = rowEnd++;
left = outerWidth - button.minWidth - skip;
}
layoutRow();
outer->resize(outerWidth, height - skip);
}, outer->lifetime());
}
void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
const auto add = [&](
rpl::producer<QString> title,
const QString &label,
const style::icon *icon,
Fn<void()> handler) {
const auto button = Settings::AddButtonWithLabel(
layout,
std::move(title),
rpl::single(label),
st::paymentsSectionButton,
{ .icon = icon });
button->addClickHandler(std::move(handler));
if (_invoice.receipt) {
button->setAttribute(Qt::WA_TransparentForMouseEvents);
}
};
add(
tr::lng_payments_payment_method(),
(_method.savedMethods.empty()
? QString()
: _method.savedMethods[_method.savedMethodIndex].title),
&st::paymentsIconPaymentMethod,
[=] { _delegate->panelEditPaymentMethod(); });
if (_invoice.isShippingAddressRequested) {
auto list = QStringList();
const auto push = [&](const QString &value) {
if (!value.isEmpty()) {
list.push_back(value);
}
};
push(_information.shippingAddress.address1);
push(_information.shippingAddress.address2);
push(_information.shippingAddress.city);
push(_information.shippingAddress.state);
push(Countries::Instance().countryNameByISO2(
_information.shippingAddress.countryIso2));
push(_information.shippingAddress.postcode);
add(
tr::lng_payments_shipping_address(),
list.join(", "),
&st::paymentsIconShippingAddress,
[=] { _delegate->panelEditShippingInformation(); });
}
if (!_options.list.empty()) {
const auto selected = ranges::find(
_options.list,
_options.selectedId,
&ShippingOption::id);
add(
tr::lng_payments_shipping_method(),
(selected != end(_options.list)) ? selected->title : QString(),
&st::paymentsIconShippingMethod,
[=] { _delegate->panelChooseShippingOption(); });
}
if (_invoice.isNameRequested) {
add(
tr::lng_payments_info_name(),
_information.name,
&st::paymentsIconName,
[=] { _delegate->panelEditName(); });
}
if (_invoice.isEmailRequested) {
add(
tr::lng_payments_info_email(),
_information.email,
&st::paymentsIconEmail,
[=] { _delegate->panelEditEmail(); });
}
if (_invoice.isPhoneRequested) {
add(
tr::lng_payments_info_phone(),
(_information.phone.isEmpty()
? QString()
: Ui::FormatPhone(_information.phone)),
&st::paymentsIconPhone,
[=] { _delegate->panelEditPhone(); });
}
Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
}
void FormSummary::setupContent(not_null<VerticalLayout*> layout) {
_scroll->widthValue(
) | rpl::on_next([=](int width) {
layout->resizeToWidth(width);
}, layout->lifetime());
setupCover(layout);
if (_invoice) {
Ui::AddDivider(layout);
setupPrices(layout);
Ui::AddDivider(layout);
setupSections(layout);
}
}
void FormSummary::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void FormSummary::updateControlsGeometry() {
const auto &padding = st::paymentsPanelPadding;
const auto buttonsHeight = padding.top()
+ _cancel->height()
+ padding.bottom();
const auto buttonsTop = height() - buttonsHeight;
_scroll->setGeometry(0, 0, width(), buttonsTop);
_topShadow->resizeToWidth(width());
_topShadow->moveToLeft(0, 0);
_bottomShadow->resizeToWidth(width());
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
auto right = padding.right();
if (_submit) {
_submit->moveToRight(right, buttonsTop + padding.top());
right += _submit->width() + padding.left();
}
_cancel->moveToRight(right, buttonsTop + padding.top());
_scroll->updateBars();
if (buttonsTop > 0 && width() > 0) {
if (const auto top = base::take(_initialScrollTop)) {
_scroll->scrollToY(top);
}
}
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,84 @@
/*
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
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
#include "styles/style_widgets.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class VerticalLayout;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class FormSummary final : public RpWidget {
public:
FormSummary(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate,
int scrollTop);
void updateThumbnail(const QImage &thumbnail);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
bool showCriticalError(const TextWithEntities &text);
[[nodiscard]] int contentHeight() const;
private:
void resizeEvent(QResizeEvent *e) override;
void setupControls();
void setupContent(not_null<VerticalLayout*> layout);
void setupCover(not_null<VerticalLayout*> layout);
void setupPrices(not_null<VerticalLayout*> layout);
void setupSuggestedTips(not_null<VerticalLayout*> layout);
void setupSections(not_null<VerticalLayout*> layout);
void updateControlsGeometry();
[[nodiscard]] QString formatAmount(
int64 amount,
bool forceStripDotZero = false) const;
[[nodiscard]] int64 computeTotalAmount() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
PaymentMethodDetails _method;
ShippingOptions _options;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
not_null<VerticalLayout*> _layout;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
rpl::event_stream<QImage> _thumbnails;
style::complex_color _tipLightBg;
style::complex_color _tipLightRipple;
style::complex_color _tipChosenBg;
style::complex_color _tipChosenRipple;
style::RoundButton _tipButton;
style::RoundButton _tipChosen;
int _initialScrollTop = 0;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,949 @@
/*
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 "payments/ui/payments_panel.h"
#include "payments/ui/payments_form_summary.h"
#include "payments/ui/payments_edit_information.h"
#include "payments/ui/payments_edit_card.h"
#include "payments/ui/payments_panel_delegate.h"
#include "payments/ui/payments_field.h"
#include "ui/widgets/separate_panel.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/boxes/single_choice_box.h"
#include "ui/chat/attach/attach_bot_webview.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/effects/radial_animation.h"
#include "ui/click_handler.h"
#include "lang/lang_keys.h"
#include "webview/webview_embed.h"
#include "webview/webview_interface.h"
#include "styles/style_payments.h"
#include "styles/style_layers.h"
namespace Payments::Ui {
namespace {
constexpr auto kProgressDuration = crl::time(200);
constexpr auto kProgressOpacity = 0.3;
} // namespace
struct Panel::Progress {
Progress(QWidget *parent, Fn<QRect()> rect);
RpWidget widget;
InfiniteRadialAnimation animation;
Animations::Simple shownAnimation;
bool shown = true;
rpl::lifetime geometryLifetime;
};
struct Panel::WebviewWithLifetime {
WebviewWithLifetime(
QWidget *parent = nullptr,
Webview::WindowConfig config = Webview::WindowConfig());
Webview::Window window;
QPointer<RpWidget> lastHidingBox;
rpl::lifetime lifetime;
};
Panel::WebviewWithLifetime::WebviewWithLifetime(
QWidget *parent,
Webview::WindowConfig config)
: window(parent, std::move(config)) {
}
Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
: widget(parent)
, animation(
[=] { if (!anim::Disabled()) widget.update(rect()); },
st::paymentsLoading) {
}
Panel::Panel(not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _widget(std::make_unique<SeparatePanel>()) {
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
_widget->setInnerSize(st::paymentsPanelSize);
_widget->closeRequests(
) | rpl::on_next([=] {
_delegate->panelRequestClose();
}, _widget->lifetime());
_widget->closeEvents(
) | rpl::on_next([=] {
_delegate->panelCloseSure();
}, _widget->lifetime());
style::PaletteChanged(
) | rpl::filter([=] {
return !_themeUpdateScheduled;
}) | rpl::on_next([=] {
_themeUpdateScheduled = true;
crl::on_main(_widget.get(), [=] {
_themeUpdateScheduled = false;
updateThemeParams(_delegate->panelWebviewThemeParams());
});
}, lifetime());
}
Panel::~Panel() {
base::take(_webview);
_progress = nullptr;
_widget = nullptr;
}
void Panel::requestActivate() {
_widget->showAndActivate();
}
void Panel::toggleProgress(bool shown) {
if (!_progress) {
if (!shown) {
return;
}
_progress = std::make_unique<Progress>(
_widget.get(),
[=] { return progressRect(); });
_progress->widget.paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(&_progress->widget);
p.setOpacity(
_progress->shownAnimation.value(_progress->shown ? 1. : 0.));
auto thickness = st::paymentsLoading.thickness;
if (progressWithBackground()) {
auto color = st::windowBg->c;
color.setAlphaF(kProgressOpacity);
p.fillRect(clip, color);
}
const auto rect = progressRect().marginsRemoved(
{ thickness, thickness, thickness, thickness });
InfiniteRadialAnimation::Draw(
p,
_progress->animation.computeState(),
rect.topLeft(),
rect.size() - QSize(),
_progress->widget.width(),
st::paymentsLoading.color,
thickness);
}, _progress->widget.lifetime());
_progress->widget.show();
_progress->animation.start();
} else if (_progress->shown == shown) {
return;
}
const auto callback = [=] {
if (!_progress->shownAnimation.animating() && !_progress->shown) {
_progress = nullptr;
} else {
_progress->widget.update();
}
};
_progress->shown = shown;
_progress->shownAnimation.start(
callback,
shown ? 0. : 1.,
shown ? 1. : 0.,
kProgressDuration);
if (shown) {
setupProgressGeometry();
}
}
bool Panel::progressWithBackground() const {
return (_progress->widget.width() == _widget->innerGeometry().width());
}
QRect Panel::progressRect() const {
const auto rect = _progress->widget.rect();
if (!progressWithBackground()) {
return rect;
}
const auto size = st::defaultBoxButton.height;
return QRect(
rect.x() + (rect.width() - size) / 2,
rect.y() + (rect.height() - size) / 2,
size,
size);
}
void Panel::setupProgressGeometry() {
if (!_progress || !_progress->shown) {
return;
}
_progress->geometryLifetime.destroy();
if (_webviewBottom) {
_webviewBottom->geometryValue(
) | rpl::on_next([=](QRect bottom) {
const auto height = bottom.height();
const auto size = st::paymentsLoading.size;
const auto skip = (height - size.height()) / 2;
const auto inner = _widget->innerGeometry();
const auto right = inner.x() + inner.width();
const auto top = inner.y() + inner.height() - height;
// This doesn't work, because first we get the correct bottom
// geometry and after that we get the previous event (which
// triggered the 'fire' of correct geometry before getting here).
//const auto right = bottom.x() + bottom.width();
//const auto top = bottom.y();
_progress->widget.setGeometry(QRect{
QPoint(right - skip - size.width(), top + skip),
size });
}, _progress->geometryLifetime);
} else if (_weakFormSummary) {
_weakFormSummary->sizeValue(
) | rpl::on_next([=](QSize form) {
const auto full = _widget->innerGeometry();
const auto size = st::defaultBoxButton.height;
const auto inner = _weakFormSummary->contentHeight();
const auto left = full.height() - inner;
if (left >= 2 * size) {
_progress->widget.setGeometry(
full.x() + (full.width() - size) / 2,
full.y() + inner + (left - size) / 2,
size,
size);
} else {
_progress->widget.setGeometry(full);
}
}, _progress->geometryLifetime);
} else if (_weakEditInformation) {
_weakEditInformation->geometryValue(
) | rpl::on_next([=] {
_progress->widget.setGeometry(_widget->innerGeometry());
}, _progress->geometryLifetime);
} else if (_weakEditCard) {
_weakEditCard->geometryValue(
) | rpl::on_next([=] {
_progress->widget.setGeometry(_widget->innerGeometry());
}, _progress->geometryLifetime);
}
_progress->widget.show();
_progress->widget.raise();
if (_progress->shown) {
_progress->widget.setFocus();
}
}
void Panel::showForm(
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options) {
if (invoice
&& method.savedMethods.empty()
&& !method.native.supported) {
const auto available = Webview::Availability();
if (available.error != Webview::Available::Error::None) {
showWebviewError(
tr::lng_payments_webview_no_use(tr::now),
available);
return;
}
}
_testMode = invoice.isTest;
setTitle(invoice.receipt
? tr::lng_payments_receipt_title()
: tr::lng_payments_checkout_title());
auto form = base::make_unique_q<FormSummary>(
_widget.get(),
invoice,
current,
method,
options,
_delegate,
_formScrollTop.current());
_weakFormSummary = form.get();
_widget->showInner(std::move(form));
_widget->setBackAllowed(false);
_formScrollTop = _weakFormSummary->scrollTopValue();
setupProgressGeometry();
}
void Panel::updateFormThumbnail(const QImage &thumbnail) {
if (_weakFormSummary) {
_weakFormSummary->updateThumbnail(thumbnail);
}
}
void Panel::showEditInformation(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field) {
setTitle(tr::lng_payments_shipping_address_title());
auto edit = base::make_unique_q<EditInformation>(
_widget.get(),
invoice,
current,
field,
_delegate);
_weakEditInformation = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditInformation->setFocusFast(field);
setupProgressGeometry();
}
void Panel::showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field) {
if (_weakEditInformation) {
_weakEditInformation->showError(field);
} else {
showEditInformation(invoice, current, field);
if (_weakEditInformation
&& field == InformationField::ShippingCountry) {
_weakEditInformation->showError(field);
}
}
}
void Panel::chooseShippingOption(const ShippingOptions &options) {
showBox(Box([=](not_null<GenericBox*> box) {
const auto i = ranges::find(
options.list,
options.selectedId,
&ShippingOption::id);
const auto index = (i != end(options.list))
? int(i - begin(options.list))
: -1;
const auto group = std::make_shared<RadiobuttonGroup>(index);
const auto layout = box->verticalLayout();
auto counter = 0;
for (const auto &option : options.list) {
const auto index = counter++;
const auto button = layout->add(
object_ptr<Radiobutton>(
layout,
group,
index,
QString(),
st::defaultBoxCheckbox,
st::defaultRadio),
st::paymentsShippingMargin);
const auto label = CreateChild<FlatLabel>(
layout.get(),
option.title,
st::paymentsShippingLabel);
const auto total = ranges::accumulate(
option.prices,
int64(0),
std::plus<>(),
&LabeledPrice::price);
const auto price = CreateChild<FlatLabel>(
layout.get(),
FillAmountAndCurrency(total, options.currency),
st::paymentsShippingPrice);
const auto area = CreateChild<AbstractButton>(layout.get());
area->setClickedCallback([=] { group->setValue(index); });
button->geometryValue(
) | rpl::on_next([=](QRect geometry) {
label->move(
geometry.topLeft() + st::paymentsShippingLabelPosition);
price->move(
geometry.topLeft() + st::paymentsShippingPricePosition);
const auto right = geometry.x()
+ st::paymentsShippingLabelPosition.x();
area->setGeometry(
right,
geometry.y(),
std::max(
label->x() + label->width() - right,
price->x() + price->width() - right),
price->y() + price->height() - geometry.y());
}, button->lifetime());
}
box->setTitle(tr::lng_payments_shipping_method());
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
group->setChangedCallback([=](int index) {
if (index >= 0) {
_delegate->panelChangeShippingOption(
options.list[index].id);
box->closeBox();
}
});
}));
}
void Panel::chooseTips(const Invoice &invoice) {
const auto max = invoice.tipsMax;
const auto now = invoice.tipsSelected;
const auto currency = invoice.currency;
showBox(Box([=](not_null<GenericBox*> box) {
box->setTitle(tr::lng_payments_tips_box_title());
const auto row = box->lifetime().make_state<Field>(
box,
FieldConfig{
.type = FieldType::Money,
.value = QString::number(now),
.currency = currency,
});
box->setFocusCallback([=] {
row->setFocusFast();
});
box->addRow(row->ownedWidget());
const auto errorWrap = box->addRow(
object_ptr<FadeWrap<FlatLabel>>(
box,
object_ptr<FlatLabel>(
box,
tr::lng_payments_tips_max(
lt_amount,
rpl::single(FillAmountAndCurrency(max, currency))),
st::paymentTipsErrorLabel)),
st::paymentTipsErrorPadding);
errorWrap->hide(anim::type::instant);
const auto submit = [=] {
const auto value = row->value().toLongLong();
if (value > max) {
row->showError();
errorWrap->show(anim::type::normal);
} else {
_delegate->panelChangeTips(value);
box->closeBox();
}
};
row->submitted(
) | rpl::on_next(submit, box->lifetime());
box->addButton(tr::lng_settings_save(), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
setTitle(tr::lng_payments_card_title());
if (method.native.supported) {
showEditCard(method.native, CardField::Number);
} else {
showEditCardByUrl(
method.url,
method.provider,
method.canSaveInformation);
}
}
void Panel::showEditCardByUrl(
const QString &url,
const QString &provider,
bool canSaveInformation) {
auto bottomText = canSaveInformation
? rpl::producer<QString>()
: tr::lng_payments_processed_by(lt_provider, rpl::single(provider));
if (!showWebview(url, true, std::move(bottomText))) {
const auto available = Webview::Availability();
if (available.error != Webview::Available::Error::None) {
showWebviewError(
tr::lng_payments_webview_no_use(tr::now),
available);
} else {
showCriticalError({ "Error: Could not initialize WebView." });
}
_widget->setBackAllowed(true);
} else if (canSaveInformation) {
const auto &padding = st::paymentsPanelPadding;
_saveWebviewInformation = CreateChild<Checkbox>(
_webviewBottom.get(),
tr::lng_payments_save_information(tr::now),
false);
const auto height = padding.top()
+ _saveWebviewInformation->heightNoMargins()
+ padding.bottom();
_saveWebviewInformation->moveToLeft(padding.right(), padding.top());
_saveWebviewInformation->show();
_webviewBottom->resize(_webviewBottom->width(), height);
}
}
void Panel::showAdditionalMethod(
const PaymentMethodAdditional &method,
const QString &provider,
bool canSaveInformation) {
setTitle(rpl::single(method.title));
showEditCardByUrl(method.url, provider, canSaveInformation);
}
void Panel::showWebviewProgress() {
if (_webviewProgress && _progress && _progress->shown) {
return;
}
_webviewProgress = true;
toggleProgress(true);
}
void Panel::hideWebviewProgress() {
if (!_webviewProgress) {
return;
}
_webviewProgress = false;
toggleProgress(false);
}
bool Panel::showWebview(
const QString &url,
bool allowBack,
rpl::producer<QString> bottomText) {
const auto params = _delegate->panelWebviewThemeParams();
if (!_webview && !createWebview(params)) {
return false;
}
showWebviewProgress();
_widget->hideLayer(anim::type::instant);
updateThemeParams(params);
_webview->window.navigate(url);
_widget->setBackAllowed(allowBack);
if (bottomText) {
const auto &padding = st::paymentsPanelPadding;
const auto label = CreateChild<FlatLabel>(
_webviewBottom.get(),
std::move(bottomText),
st::paymentsWebviewBottom);
const auto height = padding.top()
+ label->heightNoMargins()
+ padding.bottom();
rpl::combine(
_webviewBottom->widthValue(),
label->widthValue()
) | rpl::on_next([=](int outerWidth, int width) {
label->move((outerWidth - width) / 2, padding.top());
}, label->lifetime());
label->show();
_webviewBottom->resize(_webviewBottom->width(), height);
}
return true;
}
bool Panel::createWebview(const Webview::ThemeParams &params) {
auto outer = base::make_unique_q<RpWidget>(_widget.get());
const auto container = outer.get();
_widget->showInner(std::move(outer));
const auto webviewParent = QPointer<RpWidget>(container);
_webviewBottom = std::make_unique<RpWidget>(_widget.get());
const auto bottom = _webviewBottom.get();
bottom->show();
rpl::combine(
container->geometryValue() | rpl::map([=] {
return _widget->innerGeometry();
}),
bottom->heightValue()
) | rpl::on_next([=](QRect inner, int height) {
bottom->move(inner.x(), inner.y() + inner.height() - height);
bottom->resizeToWidth(inner.width());
_footerHeight = bottom->height();
}, bottom->lifetime());
container->show();
_webview = std::make_unique<WebviewWithLifetime>(
container,
Webview::WindowConfig{
.opaqueBg = params.bodyBg,
.storageId = _delegate->panelWebviewStorageId(),
});
const auto raw = &_webview->window;
QObject::connect(container, &QObject::destroyed, [=] {
if (_webview && &_webview->window == raw) {
base::take(_webview);
if (_webviewProgress) {
hideWebviewProgress();
if (_progress && !_progress->shown) {
_progress = nullptr;
}
}
}
if (_webviewBottom.get() == bottom) {
_webviewBottom = nullptr;
}
});
if (!raw->widget()) {
return false;
}
QObject::connect(raw->widget(), &QObject::destroyed, [=] {
const auto parent = webviewParent.data();
if (!_webview
|| &_webview->window != raw
|| !parent
|| _widget->inner() != parent) {
// If we destroyed _webview ourselves,
// or if we changed _widget->inner ourselves,
// we don't show any message, nothing crashed.
return;
}
crl::on_main(this, [=] {
showCriticalError({ "Error: WebView has crashed." });
});
});
rpl::combine(
container->geometryValue(),
_footerHeight.value()
) | rpl::on_next([=](QRect geometry, int footer) {
if (const auto view = raw->widget()) {
view->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));
}
}, _webview->lifetime);
raw->setMessageHandler([=](const QJsonDocument &message) {
const auto save = _saveWebviewInformation
&& _saveWebviewInformation->checked();
_delegate->panelWebviewMessage(message, save);
});
raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
if (!_delegate->panelWebviewNavigationAttempt(uri)) {
return false;
} else if (newWindow) {
return false;
}
showWebviewProgress();
return true;
});
raw->setNavigationDoneHandler([=](bool success) {
hideWebviewProgress();
});
raw->init(R"(
window.TelegramWebviewProxy = {
postEvent: function(eventType, eventData) {
if (window.external && window.external.invoke) {
window.external.invoke(JSON.stringify([eventType, eventData]));
}
}
};)");
if (!_webview) {
return false;
}
setupProgressGeometry();
return true;
}
void Panel::choosePaymentMethod(const PaymentMethodDetails &method) {
if (method.savedMethods.empty() && method.additionalMethods.empty()) {
showEditPaymentMethod(method);
return;
}
showBox(Box([=](not_null<GenericBox*> box) {
const auto save = [=](int option) {
const auto saved = int(method.savedMethods.size());
if (!option) {
showEditPaymentMethod(method);
} else if (option > saved) {
const auto index = option - saved - 1;
Assert(index < method.additionalMethods.size());
showAdditionalMethod(
method.additionalMethods[index],
method.provider,
method.canSaveInformation);
} else {
const auto index = option - 1;
_savedMethodChosen.fire_copy(method.savedMethods[index].id);
}
};
auto options = std::vector{
tr::lng_payments_new_card(tr::now),
};
for (const auto &saved : method.savedMethods) {
options.push_back(saved.title);
}
for (const auto &additional : method.additionalMethods) {
options.push_back(additional.title);
}
SingleChoiceBox(box, {
.title = tr::lng_payments_payment_method(),
.options = std::move(options),
.initialSelection = (method.savedMethods.empty()
? -1
: (method.savedMethodIndex + 1)),
.callback = save,
});
}));
}
void Panel::askSetPassword() {
showBox(Box([=](not_null<GenericBox*> box) {
box->addRow(
object_ptr<FlatLabel>(
box.get(),
tr::lng_payments_need_password(),
st::boxLabel),
st::boxPadding);
box->addButton(tr::lng_continue(), [=] {
_delegate->panelSetPassword();
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::showCloseConfirm() {
showBox(Box([=](not_null<GenericBox*> box) {
box->addRow(
object_ptr<FlatLabel>(
box.get(),
tr::lng_payments_sure_close(),
st::boxLabel),
st::boxPadding);
box->addButton(tr::lng_close(), [=] {
_delegate->panelCloseSure();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::showWarning(const QString &bot, const QString &provider) {
showBox(Box([=](not_null<GenericBox*> box) {
box->setTitle(tr::lng_payments_warning_title());
box->addRow(object_ptr<FlatLabel>(
box.get(),
tr::lng_payments_warning_body(
lt_bot1,
rpl::single(bot),
lt_provider,
rpl::single(provider),
lt_bot2,
rpl::single(bot),
lt_bot3,
rpl::single(bot)),
st::boxLabel));
box->addButton(tr::lng_continue(), [=] {
_delegate->panelTrustAndSubmit();
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::requestTermsAcceptance(
const QString &username,
const QString &url,
bool recurring) {
showBox(Box([=](not_null<GenericBox*> box) {
box->setTitle(tr::lng_payments_terms_title());
box->addRow(object_ptr<Ui::FlatLabel>(
box.get(),
(recurring
? tr::lng_payments_terms_text
: tr::lng_payments_terms_text_once)(
lt_bot,
rpl::single(tr::bold('@' + username)),
tr::marked),
st::boxLabel));
const auto update = std::make_shared<Fn<void()>>();
auto checkView = std::make_unique<Ui::CheckView>(
st::defaultCheck,
false,
[=] { if (*update) { (*update)(); } });
const auto check = checkView.get();
const auto row = box->addRow(
object_ptr<Ui::Checkbox>(
box.get(),
tr::lng_payments_terms_agree(
lt_link,
rpl::single(tr::link(
tr::lng_payments_terms_link(tr::now),
url)),
tr::marked),
st::defaultBoxCheckbox,
std::move(checkView)),
{
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::defaultBoxCheckbox.margin.bottom(),
});
row->setAllowTextLines(5);
row->setClickHandlerFilter([=](
const ClickHandlerPtr &link,
Qt::MouseButton button) {
ActivateClickHandler(_widget.get(), link, ClickContext{
.button = button,
.other = _delegate->panelClickHandlerContext(),
});
return false;
});
(*update) = [=] { row->update(); };
const auto showError = Ui::CheckView::PrepareNonToggledError(
check,
box->lifetime());
box->addButton(tr::lng_payments_terms_accept(), [=] {
if (check->checked()) {
_delegate->panelAcceptTermsAndSubmit();
box->closeBox();
} else {
showError();
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::showEditCard(
const NativeMethodDetails &native,
CardField field) {
Expects(native.supported);
auto edit = base::make_unique_q<EditCard>(
_widget.get(),
native,
field,
_delegate);
_weakEditCard = edit.get();
_widget->showInner(std::move(edit));
_widget->setBackAllowed(true);
_weakEditCard->setFocusFast(field);
setupProgressGeometry();
}
void Panel::showCardError(
const NativeMethodDetails &native,
CardField field) {
if (_weakEditCard) {
_weakEditCard->showError(field);
} else {
// We cancelled card edit already.
//showEditCard(native, field);
//if (_weakEditCard
// && field == CardField::AddressCountry) {
// _weakEditCard->showError(field);
//}
}
}
void Panel::setTitle(rpl::producer<QString> title) {
using namespace rpl::mappers;
if (_testMode) {
_widget->setTitle(std::move(title) | rpl::map(_1 + " (Test)"));
} else {
_widget->setTitle(std::move(title));
}
}
rpl::producer<> Panel::backRequests() const {
return _widget->backRequests();
}
rpl::producer<QString> Panel::savedMethodChosen() const {
return _savedMethodChosen.events();
}
void Panel::showBox(object_ptr<BoxContent> box) {
if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
const auto hideNow = !widget->isHidden();
if (hideNow || _webview->lastHidingBox) {
const auto raw = _webview->lastHidingBox = box.data();
box->boxClosing(
) | rpl::on_next([=] {
const auto widget = _webview
? _webview->window.widget()
: nullptr;
if (widget
&& widget->isHidden()
&& _webview->lastHidingBox == raw) {
widget->show();
}
}, _webview->lifetime);
if (hideNow) {
widget->hide();
}
}
}
_widget->showBox(
std::move(box),
LayerOption::KeepOther,
anim::type::normal);
}
void Panel::showToast(TextWithEntities &&text) {
_widget->showToast(std::move(text));
}
void Panel::showCriticalError(const TextWithEntities &text) {
_progress = nullptr;
_webviewProgress = false;
if (!_weakFormSummary || !_weakFormSummary->showCriticalError(text)) {
auto wrap = base::make_unique_q<RpWidget>(_widget.get());
const auto raw = wrap.get();
const auto error = CreateChild<PaddingWrap<FlatLabel>>(
raw,
object_ptr<FlatLabel>(
raw,
rpl::single(text),
st::paymentsCriticalError),
st::paymentsCriticalErrorPadding);
error->entity()->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton) {
const auto entity = handler->getTextEntity();
if (entity.type != EntityType::CustomUrl) {
return true;
}
_delegate->panelOpenUrl(entity.data);
return false;
});
raw->widthValue() | rpl::on_next([=](int width) {
error->resizeToWidth(width);
raw->resize(width, error->height());
}, raw->lifetime());
_widget->showInner(std::move(wrap));
}
}
std::shared_ptr<Show> Panel::uiShow() {
return _widget->uiShow();
}
void Panel::showWebviewError(
const QString &text,
const Webview::Available &information) {
showCriticalError(TextWithEntities{ text }.append(
"\n\n"
).append(BotWebView::ErrorText(information)));
}
void Panel::updateThemeParams(const Webview::ThemeParams &params) {
if (!_webview || !_webview->window.widget()) {
return;
}
_webview->window.updateTheme(
params.bodyBg,
params.scrollBg,
params.scrollBgOver,
params.scrollBarBg,
params.scrollBarBgOver);
_webview->window.eval(R"(
if (window.TelegramGameProxy) {
window.TelegramGameProxy.receiveEvent(
"theme_changed",
{ "theme_params": )" + params.json + R"( });
}
)");
}
rpl::lifetime &Panel::lifetime() {
return _widget->lifetime();
}
} // namespace Payments::Ui

View File

@@ -0,0 +1,138 @@
/*
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
*/
#pragma once
#include "base/object_ptr.h"
#include "base/weak_ptr.h"
namespace Ui {
class Show;
class RpWidget;
class SeparatePanel;
class BoxContent;
class Checkbox;
} // namespace Ui
namespace Webview {
struct Available;
struct ThemeParams;
} // namespace Webview
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
struct Invoice;
struct RequestedInformation;
struct ShippingOptions;
enum class InformationField;
enum class CardField;
class FormSummary;
class EditInformation;
class EditCard;
struct PaymentMethodDetails;
struct PaymentMethodAdditional;
struct NativeMethodDetails;
class Panel final : public base::has_weak_ptr {
public:
explicit Panel(not_null<PanelDelegate*> delegate);
~Panel();
void requestActivate();
void toggleProgress(bool shown);
void showForm(
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options);
void updateFormThumbnail(const QImage &thumbnail);
void showEditInformation(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field);
void showInformationError(
const Invoice &invoice,
const RequestedInformation &current,
InformationField field);
void showEditPaymentMethod(const PaymentMethodDetails &method);
void showAdditionalMethod(
const PaymentMethodAdditional &method,
const QString &provider,
bool canSaveInformation);
void showEditCard(const NativeMethodDetails &native, CardField field);
void showEditCardByUrl(
const QString &url,
const QString &provider,
bool canSaveInformation);
void showCardError(const NativeMethodDetails &native, CardField field);
void chooseShippingOption(const ShippingOptions &options);
void chooseTips(const Invoice &invoice);
void choosePaymentMethod(const PaymentMethodDetails &method);
void askSetPassword();
void showCloseConfirm();
void showWarning(const QString &bot, const QString &provider);
void requestTermsAcceptance(
const QString &username,
const QString &url,
bool recurring);
bool showWebview(
const QString &url,
bool allowBack,
rpl::producer<QString> bottomText);
void updateThemeParams(const Webview::ThemeParams &params);
[[nodiscard]] rpl::producer<> backRequests() const;
[[nodiscard]] rpl::producer<QString> savedMethodChosen() const;
void showBox(object_ptr<Ui::BoxContent> box);
void showToast(TextWithEntities &&text);
void showCriticalError(const TextWithEntities &text);
[[nodiscard]] std::shared_ptr<Show> uiShow();
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Progress;
struct WebviewWithLifetime;
bool createWebview(const Webview::ThemeParams &params);
void showWebviewProgress();
void hideWebviewProgress();
void showWebviewError(
const QString &text,
const Webview::Available &information);
void setTitle(rpl::producer<QString> title);
[[nodiscard]] bool progressWithBackground() const;
[[nodiscard]] QRect progressRect() const;
void setupProgressGeometry();
void updateFooterHeight();
const not_null<PanelDelegate*> _delegate;
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<WebviewWithLifetime> _webview;
std::unique_ptr<RpWidget> _webviewBottom;
rpl::variable<int> _footerHeight;
std::unique_ptr<Progress> _progress;
QPointer<Checkbox> _saveWebviewInformation;
QPointer<FormSummary> _weakFormSummary;
rpl::variable<int> _formScrollTop;
QPointer<EditInformation> _weakEditInformation;
QPointer<EditCard> _weakEditCard;
rpl::event_stream<QString> _savedMethodChosen;
bool _themeUpdateScheduled = false;
bool _webviewProgress = false;
bool _testMode = false;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,204 @@
/*
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
*/
#pragma once
#include "ui/text/text_entity.h"
namespace Payments::Ui {
struct LabeledPrice {
QString label;
int64 price = 0;
};
struct Cover {
QString title;
TextWithEntities description;
QString seller;
QImage thumbnail;
};
struct Receipt {
TimeId date = 0;
int64 totalAmount = 0;
QString currency;
bool paid = false;
[[nodiscard]] bool empty() const {
return !paid;
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct Invoice {
Cover cover;
std::vector<LabeledPrice> prices;
std::vector<int64> suggestedTips;
int64 tipsMax = 0;
int64 tipsSelected = 0;
QString currency;
Receipt receipt;
bool isNameRequested = false;
bool isPhoneRequested = false;
bool isEmailRequested = false;
bool isShippingAddressRequested = false;
bool isRecurring = false;
bool isFlexible = false;
bool isTest = false;
QString provider;
QString termsUrl;
bool phoneSentToProvider = false;
bool emailSentToProvider = false;
[[nodiscard]] bool valid() const {
return !currency.isEmpty() && (!prices.empty() || tipsMax);
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct ShippingOption {
QString id;
QString title;
std::vector<LabeledPrice> prices;
};
struct ShippingOptions {
QString currency;
std::vector<ShippingOption> list;
QString selectedId;
};
struct Address {
QString address1;
QString address2;
QString city;
QString state;
QString countryIso2;
QString postcode;
[[nodiscard]] bool valid() const {
return !address1.isEmpty()
&& !city.isEmpty()
&& !countryIso2.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
inline bool operator==(const Address &other) const {
return (address1 == other.address1)
&& (address2 == other.address2)
&& (city == other.city)
&& (state == other.state)
&& (countryIso2 == other.countryIso2)
&& (postcode == other.postcode);
}
inline bool operator!=(const Address &other) const {
return !(*this == other);
}
};
struct RequestedInformation {
QString defaultPhone;
QString defaultCountry;
bool save = true;
QString name;
QString phone;
QString email;
Address shippingAddress;
[[nodiscard]] bool empty() const {
return name.isEmpty()
&& phone.isEmpty()
&& email.isEmpty()
&& !shippingAddress;
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
inline bool operator==(const RequestedInformation &other) const {
return (name == other.name)
&& (phone == other.phone)
&& (email == other.email)
&& (shippingAddress == other.shippingAddress);
}
inline bool operator!=(const RequestedInformation &other) const {
return !(*this == other);
}
};
enum class InformationField {
ShippingStreet,
ShippingCity,
ShippingState,
ShippingCountry,
ShippingPostcode,
Name,
Email,
Phone,
};
struct NativeMethodDetails {
QString defaultCountry;
bool supported = false;
bool needCountry = false;
bool needZip = false;
bool needCardholderName = false;
bool canSaveInformation = false;
};
struct PaymentMethodAdditional {
QString title;
QString url;
};
struct PaymentMethodSaved {
QString id;
QString title;
};
struct PaymentMethodDetails {
NativeMethodDetails native;
std::vector<PaymentMethodSaved> savedMethods;
std::vector<PaymentMethodAdditional> additionalMethods;
QString url;
QString provider;
int savedMethodIndex = 0;
bool canSaveInformation = false;
};
enum class CardField {
Number,
Cvc,
ExpireDate,
Name,
AddressCountry,
AddressZip,
};
struct UncheckedCardDetails {
QString number;
QString cvc;
uint32 expireYear = 0;
uint32 expireMonth = 0;
QString cardholderName;
QString addressCountry;
QString addressZip;
};
} // namespace Payments::Ui

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "base/object_ptr.h"
class QJsonDocument;
class QString;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Webview {
struct ThemeParams;
struct StorageId;
} // namespace Webview
namespace Payments::Ui {
using namespace ::Ui;
struct RequestedInformation;
struct UncheckedCardDetails;
class PanelDelegate {
public:
virtual void panelRequestClose() = 0;
virtual void panelCloseSure() = 0;
virtual void panelSubmit() = 0;
virtual void panelTrustAndSubmit() = 0;
virtual void panelAcceptTermsAndSubmit() = 0;
virtual void panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) = 0;
virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;
virtual void panelSetPassword() = 0;
virtual void panelOpenUrl(const QString &url) = 0;
virtual void panelCancelEdit() = 0;
virtual void panelEditPaymentMethod() = 0;
virtual void panelEditShippingInformation() = 0;
virtual void panelEditName() = 0;
virtual void panelEditEmail() = 0;
virtual void panelEditPhone() = 0;
virtual void panelChooseShippingOption() = 0;
virtual void panelChangeShippingOption(const QString &id) = 0;
virtual void panelChooseTips() = 0;
virtual void panelChangeTips(int64 value) = 0;
virtual void panelValidateInformation(RequestedInformation data) = 0;
virtual void panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) = 0;
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
virtual QVariant panelClickHandlerContext() = 0;
virtual Webview::StorageId panelWebviewStorageId() = 0;
virtual Webview::ThemeParams panelWebviewThemeParams() = 0;
virtual std::optional<QDate> panelOverrideExpireDateThreshold() = 0;
};
} // namespace Payments::Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
/*
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
*/
#pragma once
#include "base/object_ptr.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
namespace style {
struct RoundCheckbox;
struct MediaSlider;
} // namespace style
namespace Main {
class Session;
} // namespace Main
namespace Ui::Premium {
class BubbleWidget;
} // namespace Ui::Premium
namespace Ui {
class AbstractButton;
class BoxContent;
class GenericBox;
class DynamicImage;
class VerticalLayout;
struct PaidReactionTop {
QString name;
std::shared_ptr<DynamicImage> photo;
uint64 barePeerId = 0;
int count = 0;
Fn<void()> click;
bool my = false;
};
struct PaidReactionBoxArgs {
int min = 0;
int explicitlyAllowed = 0;
int chosen = 0;
int max = 0;
std::vector<PaidReactionTop> top;
not_null<Main::Session*> session;
QString name;
Fn<rpl::producer<TextWithEntities>(rpl::producer<int> amount)> submit;
std::vector<Calls::Group::Ui::StarsColoring> colorings;
rpl::producer<CreditsAmount> balanceValue;
Fn<void(int, uint64)> send;
bool videoStreamChoosing = false;
bool videoStreamSending = false;
bool videoStreamAdmin = false;
bool dark = false;
};
void PaidReactionsBox(
not_null<GenericBox*> box,
PaidReactionBoxArgs &&args);
[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(
PaidReactionBoxArgs &&args);
[[nodiscard]] int MaxTopPaidDonorsShown();
[[nodiscard]] QImage GenerateSmallBadgeImage(
QString text,
const style::icon &icon,
QColor bg,
QColor fg,
const style::RoundCheckbox *borderSt = nullptr);
struct StarSelectDiscreter {
Fn<int(float64)> ratioToValue;
Fn<float64(int)> valueToRatio;
};
[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max);
void PaidReactionSlider(
not_null<VerticalLayout*> container,
const style::MediaSlider &st,
int min,
int explicitlyAllowed,
rpl::producer<int> current,
int max,
Fn<void(int)> changed,
Fn<QColor(int)> activeFgOverride = nullptr);
void AddStarSelectBalance(
not_null<GenericBox*> box,
not_null<Main::Session*> session,
rpl::producer<CreditsAmount> balanceValue,
bool dark = false);
not_null<Premium::BubbleWidget*> AddStarSelectBubble(
not_null<VerticalLayout*> container,
rpl::producer<> showFinishes,
rpl::producer<int> value,
int max,
Fn<QColor(int)> activeFgOverride = nullptr);
struct StarSelectInfoBlock {
rpl::producer<TextWithEntities> title;
rpl::producer<QString> subtext;
Fn<void()> click;
};
[[nodiscard]] object_ptr<RpWidget> MakeStarSelectInfoBlocks(
not_null<RpWidget*> parent,
std::vector<StarSelectInfoBlock> blocks,
Text::MarkedContext context,
bool dark = false);
} // namespace Ui