434 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			434 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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) {
 | 
						|
	return value.replace(QRegularExpression("[^0-9]"), 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() {
 | 
						|
	return ComplexNumberValidator(
 | 
						|
		Stripe::ValidateExpireDate,
 | 
						|
		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::start_with_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(),
 | 
						|
	});
 | 
						|
	_cvc = make(container, {
 | 
						|
		.type = FieldType::CardCVC,
 | 
						|
		.placeholder = tr::lng_payments_card_cvc(),
 | 
						|
		.validator = CvcValidator([=] { return _number->value(); }),
 | 
						|
	});
 | 
						|
	container->widthValue(
 | 
						|
	) | rpl::start_with_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::start_with_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::start_with_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
 |