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
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:
156
Telegram/SourceFiles/payments/ui/payments.style
Normal file
156
Telegram/SourceFiles/payments/ui/payments.style
Normal 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;
|
||||
}
|
||||
437
Telegram/SourceFiles/payments/ui/payments_edit_card.cpp
Normal file
437
Telegram/SourceFiles/payments/ui/payments_edit_card.cpp
Normal 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
|
||||
72
Telegram/SourceFiles/payments/ui/payments_edit_card.h
Normal file
72
Telegram/SourceFiles/payments/ui/payments_edit_card.h
Normal 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
|
||||
282
Telegram/SourceFiles/payments/ui/payments_edit_information.cpp
Normal file
282
Telegram/SourceFiles/payments/ui/payments_edit_information.cpp
Normal 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 ¤t,
|
||||
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
|
||||
80
Telegram/SourceFiles/payments/ui/payments_edit_information.h
Normal file
80
Telegram/SourceFiles/payments/ui/payments_edit_information.h
Normal 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 ¤t,
|
||||
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
|
||||
719
Telegram/SourceFiles/payments/ui/payments_field.cpp
Normal file
719
Telegram/SourceFiles/payments/ui/payments_field.cpp
Normal 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
|
||||
143
Telegram/SourceFiles/payments/ui/payments_field.h
Normal file
143
Telegram/SourceFiles/payments/ui/payments_field.h
Normal 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
|
||||
612
Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
Normal file
612
Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
Normal 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 ¤t,
|
||||
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
|
||||
84
Telegram/SourceFiles/payments/ui/payments_form_summary.h
Normal file
84
Telegram/SourceFiles/payments/ui/payments_form_summary.h
Normal 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 ¤t,
|
||||
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
|
||||
949
Telegram/SourceFiles/payments/ui/payments_panel.cpp
Normal file
949
Telegram/SourceFiles/payments/ui/payments_panel.cpp
Normal 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 ¤t,
|
||||
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 ¤t,
|
||||
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 ¤t,
|
||||
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 ¶ms) {
|
||||
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 ¶ms) {
|
||||
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
|
||||
138
Telegram/SourceFiles/payments/ui/payments_panel.h
Normal file
138
Telegram/SourceFiles/payments/ui/payments_panel.h
Normal 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 ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options);
|
||||
void updateFormThumbnail(const QImage &thumbnail);
|
||||
void showEditInformation(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field);
|
||||
void showInformationError(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
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 ¶ms);
|
||||
|
||||
[[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 ¶ms);
|
||||
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
|
||||
204
Telegram/SourceFiles/payments/ui/payments_panel_data.h
Normal file
204
Telegram/SourceFiles/payments/ui/payments_panel_data.h
Normal 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
|
||||
69
Telegram/SourceFiles/payments/ui/payments_panel_delegate.h
Normal file
69
Telegram/SourceFiles/payments/ui/payments_panel_delegate.h
Normal 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
|
||||
1035
Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp
Normal file
1035
Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
120
Telegram/SourceFiles/payments/ui/payments_reaction_box.h
Normal file
120
Telegram/SourceFiles/payments/ui/payments_reaction_box.h
Normal 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
|
||||
Reference in New Issue
Block a user