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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

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

View File

@@ -0,0 +1,978 @@
/*
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/payments_checkout_process.h"
#include "payments/payments_form.h"
#include "payments/ui/payments_panel.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "storage/storage_account.h"
#include "history/history_item.h"
#include "history/history.h"
#include "data/data_user.h" // UserData::isBot.
#include "boxes/passcode_box.h"
#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
#include "core/file_utilities.h" // File::OpenUrl.
#include "core/core_cloud_password.h" // Core::CloudPasswordState
#include "core/click_handler_types.h"
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include "api/api_cloud_password.h"
#include "window/themes/window_theme.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
namespace Payments {
namespace {
struct SessionProcesses {
base::flat_map<FullMsgId, std::unique_ptr<CheckoutProcess>> byItem;
base::flat_map<QString, std::unique_ptr<CheckoutProcess>> bySlug;
base::flat_map<uint64, std::unique_ptr<CheckoutProcess>> byRandomId;
base::flat_map<FullMsgId, PaidInvoice> paymentStartedByItem;
base::flat_map<QString, PaidInvoice> paymentStartedBySlug;
rpl::lifetime lifetime;
};
base::flat_map<not_null<Main::Session*>, SessionProcesses> Processes;
[[nodiscard]] SessionProcesses &LookupSessionProcesses(
not_null<Main::Session*> session) {
const auto i = Processes.find(session);
if (i != end(Processes)) {
return i->second;
}
const auto j = Processes.emplace(session).first;
auto &result = j->second;
session->account().sessionChanges(
) | rpl::on_next([=] {
Processes.erase(session);
}, result.lifetime);
return result;
}
} // namespace
void CheckoutProcess::Start(
not_null<const HistoryItem*> item,
Mode mode,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
auto &processes = LookupSessionProcesses(&item->history()->session());
const auto media = item->media();
const auto invoice = media ? media->invoice() : nullptr;
if (mode == Mode::Payment && !invoice) {
return;
}
const auto id = (invoice && invoice->receiptMsgId)
? FullMsgId(item->history()->peer->id, invoice->receiptMsgId)
: item->fullId();
if (invoice) {
mode = invoice->receiptMsgId ? Mode::Receipt : Mode::Payment;
} else if (mode == Mode::Payment) {
LOG(("API Error: CheckoutProcess Payment start without invoice."));
return;
}
const auto i = processes.byItem.find(id);
if (i != end(processes.byItem)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
i->second->requestActivate();
return;
}
const auto j = processes.byItem.emplace(
id,
std::make_unique<CheckoutProcess>(
InvoiceId{ InvoiceMessage{ item->history()->peer, id.msg } },
mode,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first;
j->second->requestActivate();
}
void CheckoutProcess::Start(
not_null<Main::Session*> session,
const QString &slug,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
auto &processes = LookupSessionProcesses(session);
const auto i = processes.bySlug.find(slug);
if (i != end(processes.bySlug)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
i->second->requestActivate();
return;
}
const auto j = processes.bySlug.emplace(
slug,
std::make_unique<CheckoutProcess>(
InvoiceId{ InvoiceSlug{ session, slug } },
Mode::Payment,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first;
j->second->requestActivate();
}
void CheckoutProcess::Start(
InvoicePremiumGiftCode giftCodeInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto randomId = giftCodeInvoice.randomId;
auto id = InvoiceId{ std::move(giftCodeInvoice) };
auto &processes = LookupSessionProcesses(SessionFromId(id));
const auto i = processes.byRandomId.find(randomId);
if (i != end(processes.byRandomId)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
i->second->requestActivate();
return;
}
const auto j = processes.byRandomId.emplace(
randomId,
std::make_unique<CheckoutProcess>(
std::move(id),
Mode::Payment,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{})).first;
j->second->requestActivate();
}
void CheckoutProcess::Start(
InvoiceCredits creditsInvoice,
Fn<void(CheckoutResult)> reactivate) {
const auto randomId = creditsInvoice.randomId;
auto id = InvoiceId{ std::move(creditsInvoice) };
auto &processes = LookupSessionProcesses(SessionFromId(id));
const auto i = processes.byRandomId.find(randomId);
if (i != end(processes.byRandomId)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->requestActivate();
return;
}
const auto j = processes.byRandomId.emplace(
randomId,
std::make_unique<CheckoutProcess>(
std::move(id),
Mode::Payment,
std::move(reactivate),
nullptr,
PrivateTag{})).first;
j->second->requestActivate();
}
void CheckoutProcess::Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess) {
const auto randomId = giftInvoice.randomId;
auto id = InvoiceId{ std::move(giftInvoice) };
auto &processes = LookupSessionProcesses(SessionFromId(id));
const auto i = processes.byRandomId.find(randomId);
if (i != end(processes.byRandomId)) {
i->second->setReactivateCallback(std::move(reactivate));
i->second->setNonPanelPaymentFormProcess(
std::move(nonPanelPaymentFormProcess));
return;
}
processes.byRandomId.emplace(
randomId,
std::make_unique<CheckoutProcess>(
std::move(id),
Mode::Payment,
std::move(reactivate),
std::move(nonPanelPaymentFormProcess),
PrivateTag{}));
}
std::optional<PaidInvoice> CheckoutProcess::InvoicePaid(
not_null<const HistoryItem*> item) {
const auto session = &item->history()->session();
const auto itemId = item->fullId();
const auto i = Processes.find(session);
if (i == end(Processes)) {
return std::nullopt;
}
const auto k = i->second.paymentStartedByItem.find(itemId);
if (k == end(i->second.paymentStartedByItem)) {
return std::nullopt;
}
const auto result = k->second;
i->second.paymentStartedByItem.erase(k);
const auto j = i->second.byItem.find(itemId);
if (j != end(i->second.byItem)) {
j->second->closeAndReactivate(CheckoutResult::Paid);
} else if (i->second.paymentStartedByItem.empty()
&& i->second.byItem.empty()
&& i->second.paymentStartedBySlug.empty()
&& i->second.bySlug.empty()
&& i->second.byRandomId.empty()) {
Processes.erase(i);
}
return result;
}
std::optional<PaidInvoice> CheckoutProcess::InvoicePaid(
not_null<Main::Session*> session,
const QString &slug) {
const auto i = Processes.find(session);
if (i == end(Processes)) {
return std::nullopt;
}
const auto k = i->second.paymentStartedBySlug.find(slug);
if (k == end(i->second.paymentStartedBySlug)) {
return std::nullopt;
}
const auto result = k->second;
i->second.paymentStartedBySlug.erase(k);
const auto j = i->second.bySlug.find(slug);
if (j != end(i->second.bySlug)) {
j->second->closeAndReactivate(CheckoutResult::Paid);
} else if (i->second.paymentStartedByItem.empty()
&& i->second.byItem.empty()
&& i->second.paymentStartedBySlug.empty()
&& i->second.bySlug.empty()
&& i->second.byRandomId.empty()) {
Processes.erase(i);
}
return result;
}
void CheckoutProcess::ClearAll() {
Processes.clear();
}
void CheckoutProcess::RegisterPaymentStart(
not_null<CheckoutProcess*> process,
PaidInvoice info) {
const auto i = Processes.find(process->_session);
Assert(i != end(Processes));
for (const auto &[itemId, itemProcess] : i->second.byItem) {
if (itemProcess.get() == process) {
i->second.paymentStartedByItem.emplace(itemId, info);
return;
}
}
for (const auto &[slug, itemProcess] : i->second.bySlug) {
if (itemProcess.get() == process) {
i->second.paymentStartedBySlug.emplace(slug, info);
return;
}
}
for (const auto &[randomId, itemProcess] : i->second.byRandomId) {
if (itemProcess.get() == process) {
return;
}
}
}
void CheckoutProcess::UnregisterPaymentStart(
not_null<CheckoutProcess*> process) {
const auto i = Processes.find(process->_session);
if (i == end(Processes)) {
return;
}
for (const auto &[itemId, itemProcess] : i->second.byItem) {
if (itemProcess.get() == process) {
i->second.paymentStartedByItem.remove(itemId);
break;
}
}
for (const auto &[slug, itemProcess] : i->second.bySlug) {
if (itemProcess.get() == process) {
i->second.paymentStartedBySlug.remove(slug);
break;
}
}
for (const auto &[randomId, itemProcess] : i->second.byRandomId) {
if (itemProcess.get() == process) {
break;
}
}
if (i->second.paymentStartedByItem.empty()
&& i->second.byItem.empty()
&& i->second.paymentStartedBySlug.empty()
&& i->second.bySlug.empty()
&& i->second.byRandomId.empty()) {
Processes.erase(i);
}
}
CheckoutProcess::CheckoutProcess(
InvoiceId id,
Mode mode,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess,
PrivateTag)
: _session(SessionFromId(id))
, _form(std::make_unique<Form>(id, (mode == Mode::Receipt)))
, _panel(std::make_unique<Ui::Panel>(panelDelegate()))
, _reactivate(std::move(reactivate))
, _nonPanelPaymentFormProcess(std::move(nonPanelPaymentFormProcess)) {
_form->updates(
) | rpl::on_next([=](const FormUpdate &update) {
handleFormUpdate(update);
}, _lifetime);
_panel->savedMethodChosen(
) | rpl::on_next([=](QString id) {
_form->chooseSavedMethod(id);
}, _panel->lifetime());
_panel->backRequests(
) | rpl::on_next([=] {
panelCancelEdit();
}, _panel->lifetime());
if (!_nonPanelPaymentFormProcess) {
showForm();
}
_panel->toggleProgress(true);
if (mode == Mode::Payment) {
_session->api().cloudPassword().state(
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
_form->setHasPassword(state.hasPassword);
}, _lifetime);
}
}
CheckoutProcess::~CheckoutProcess() {
}
void CheckoutProcess::setReactivateCallback(
Fn<void(CheckoutResult)> reactivate) {
_reactivate = std::move(reactivate);
}
void CheckoutProcess::setNonPanelPaymentFormProcess(
Fn<void(NonPanelPaymentForm)> callback) {
_nonPanelPaymentFormProcess = std::move(callback);
}
void CheckoutProcess::requestActivate() {
if (!_nonPanelPaymentFormProcess) {
_panel->requestActivate();
}
}
not_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() {
return static_cast<PanelDelegate*>(this);
}
void CheckoutProcess::handleFormUpdate(const FormUpdate &update) {
v::match(update, [&](const ToggleProgress &data) {
_panel->toggleProgress(data.shown);
}, [&](const FormReady &) {
performInitialSilentValidation();
if (!_initialSilentValidation) {
showForm();
}
if (!_form->paymentMethod().savedCredentials.empty()) {
_session->api().cloudPassword().reload();
}
}, [&](const ThumbnailUpdated &data) {
_panel->updateFormThumbnail(data.thumbnail);
}, [&](const ValidateFinished &) {
if (_initialSilentValidation) {
_initialSilentValidation = false;
}
showForm();
const auto submitted = (_submitState == SubmitState::Validating);
_submitState = SubmitState::Validated;
if (submitted) {
panelSubmit();
}
}, [&](const PaymentMethodUpdate &data) {
showForm();
if (data.requestNewPassword) {
requestSetPassword();
}
}, [&](const TmpPasswordRequired &) {
UnregisterPaymentStart(this);
_submitState = SubmitState::Validated;
requestPassword();
}, [&](const BotTrustRequired &data) {
UnregisterPaymentStart(this);
_submitState = SubmitState::Validated;
_panel->showWarning(data.bot->name(), data.provider->name());
if (const auto box = _enterPasswordBox.get()) {
box->closeBox();
}
}, [&](const VerificationNeeded &data) {
auto bottomText = tr::lng_payments_processed_by(
lt_provider,
rpl::single(_form->invoice().provider));
_sendFormFailed = false;
_sendFormPending = true;
if (!_panel->showWebview(data.url, false, std::move(bottomText))) {
File::OpenUrl(data.url);
close();
}
}, [&](const PaymentFinished &data) {
const auto weak = base::make_weak(this);
_session->api().applyUpdates(data.updates);
if (weak) {
closeAndReactivate(CheckoutResult::Paid);
}
}, [&](const CreditsPaymentStarted &data) {
if (_nonPanelPaymentFormProcess) {
_nonPanelPaymentFormProcess(
std::make_shared<CreditsFormData>(data.data));
close();
}
}, [&](const CreditsReceiptReady &data) {
if (_nonPanelPaymentFormProcess) {
_nonPanelPaymentFormProcess(
std::make_shared<CreditsReceiptData>(data.data));
close();
}
}, [&](const Error &error) {
handleError(error);
});
}
void CheckoutProcess::handleError(const Error &error) {
const auto showToast = [&](TextWithEntities &&text) {
_panel->requestActivate();
_panel->showToast(std::move(text));
};
const auto &id = error.id;
switch (error.type) {
case Error::Type::Form:
if (id == u"INVOICE_ALREADY_PAID"_q) {
_panel->showCriticalError({
tr::lng_payments_already_paid(tr::now)
});
} else if (true
|| id == u"PROVIDER_ACCOUNT_INVALID"_q
|| id == u"PROVIDER_ACCOUNT_TIMEOUT"_q) {
_panel->showCriticalError({ "Error: " + id });
}
break;
case Error::Type::Validate: {
if (_submitState == SubmitState::Validating
|| _submitState == SubmitState::Validated) {
_submitState = SubmitState::None;
}
if (_initialSilentValidation) {
_initialSilentValidation = false;
showForm();
return;
}
using InfoField = Ui::InformationField;
using CardField = Ui::CardField;
if (id == u"REQ_INFO_NAME_INVALID"_q) {
showInformationError(InfoField::Name);
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
showInformationError(InfoField::Email);
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
showInformationError(InfoField::Phone);
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
showInformationError(InfoField::ShippingStreet);
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
showInformationError(InfoField::ShippingCity);
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
showInformationError(InfoField::ShippingState);
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
showInformationError(InfoField::ShippingCountry);
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
showInformationError(InfoField::ShippingPostcode);
} else if (id == u"LOCAL_CARD_NUMBER_INVALID"_q) {
showCardError(CardField::Number);
} else if (id == u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q) {
showCardError(CardField::ExpireDate);
} else if (id == u"LOCAL_CARD_CVC_INVALID"_q) {
showCardError(CardField::Cvc);
} else if (id == u"LOCAL_CARD_HOLDER_NAME_INVALID"_q) {
showCardError(CardField::Name);
} else if (id == u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q) {
showCardError(CardField::AddressCountry);
} else if (id == u"LOCAL_CARD_BILLING_ZIP_INVALID"_q) {
showCardError(CardField::AddressZip);
} else if (id == u"SHIPPING_BOT_TIMEOUT"_q) {
showToast({ "Error: Bot Timeout!" });
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
showToast({ tr::lng_payments_shipping_not_available(tr::now) });
} else {
showToast({ "Error: " + id });
}
} break;
case Error::Type::Stripe: {
using Field = Ui::CardField;
if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) {
showCardError(Field::Number);
} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
showCardError(Field::Cvc);
} else if (id == u"InvalidExpiryMonth"_q
|| id == u"InvalidExpiryYear"_q
|| id == u"ExpiredCard"_q) {
showCardError(Field::ExpireDate);
} else if (id == u"CardDeclined"_q) {
showToast({ tr::lng_payments_card_declined(tr::now) });
} else if (id == u"ProcessingError"_q) {
showToast({ "Sorry, a processing error occurred." });
} else {
showToast({ "Stripe Error: " + id });
}
} break;
case Error::Type::SmartGlocal: {
showToast({ "SmartGlocal Error: " + id });
} break;
case Error::Type::TmpPassword:
if (const auto box = _enterPasswordBox.get()) {
if (!box->handleCustomCheckError(id)) {
showToast({ "Error: Could not generate tmp password." });
}
}
break;
case Error::Type::Send:
_sendFormFailed = true;
if (const auto box = _enterPasswordBox.get()) {
box->closeBox();
}
if (_submitState == SubmitState::Finishing) {
UnregisterPaymentStart(this);
_submitState = SubmitState::Validated;
}
if (id == u"INVOICE_ALREADY_PAID"_q) {
showToast({ tr::lng_payments_already_paid(tr::now) });
} else if (id == u"PAYMENT_FAILED"_q) {
showToast({ tr::lng_payments_payment_failed(tr::now) });
} else if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
showToast({ tr::lng_payments_precheckout_failed(tr::now) });
} else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
showToast({ tr::lng_payments_precheckout_timeout(tr::now) });
} else if (id == u"REQUESTED_INFO_INVALID"_q
|| id == u"SHIPPING_OPTION_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) {
showToast({ tr::lng_payments_payment_failed(tr::now) });
showToast({ "Error: " + id + ". Your card has not been billed." });
} else if (id == u"TMP_PASSWORD_INVALID"_q) {
requestPassword();
} else {
showToast({ "Error: " + id });
}
break;
default: Unexpected("Error type in CheckoutProcess::handleError.");
}
}
void CheckoutProcess::panelRequestClose() {
if (_form->hasChanges()) {
_panel->showCloseConfirm();
} else {
panelCloseSure();
}
}
void CheckoutProcess::panelCloseSure() {
closeAndReactivate(_sendFormFailed
? CheckoutResult::Failed
: _sendFormPending
? CheckoutResult::Pending
: CheckoutResult::Cancelled);
}
void CheckoutProcess::closeAndReactivate(CheckoutResult result) {
const auto reactivate = std::move(_reactivate);
close();
if (reactivate) {
reactivate(result);
}
}
void CheckoutProcess::close() {
const auto i = Processes.find(_session);
if (i == end(Processes)) {
return;
}
auto &entry = i->second;
const auto j = ranges::find(entry.byItem, this, [](const auto &pair) {
return pair.second.get();
});
if (j != end(entry.byItem)) {
entry.byItem.erase(j);
}
const auto k = ranges::find(entry.bySlug, this, [](const auto &pair) {
return pair.second.get();
});
if (k != end(entry.bySlug)) {
entry.bySlug.erase(k);
}
const auto l = ranges::find(
entry.byRandomId,
this,
[](const auto &pair) { return pair.second.get(); });
if (l != end(entry.byRandomId)) {
entry.byRandomId.erase(l);
}
if (entry.byItem.empty()
&& entry.bySlug.empty()
&& i->second.byRandomId.empty()
&& entry.paymentStartedByItem.empty()
&& entry.paymentStartedBySlug.empty()) {
Processes.erase(i);
}
}
void CheckoutProcess::panelSubmit() {
if (_form->invoice().receipt.paid) {
closeAndReactivate(CheckoutResult::Paid);
return;
} else if (_submitState == SubmitState::Validating
|| _submitState == SubmitState::Finishing) {
return;
}
const auto &method = _form->paymentMethod();
const auto &invoice = _form->invoice();
const auto &options = _form->shippingOptions();
if (!options.list.empty() && options.selectedId.isEmpty()) {
chooseShippingOption();
} else if (_submitState != SubmitState::Validated
&& options.list.empty()
&& (invoice.isShippingAddressRequested
|| invoice.isNameRequested
|| invoice.isEmailRequested
|| invoice.isPhoneRequested)) {
_submitState = SubmitState::Validating;
_form->validateInformation(_form->information());
} else if (!method.newCredentials
&& method.savedCredentialsIndex >= method.savedCredentials.size()) {
editPaymentMethod();
} else if (!invoice.termsUrl.isEmpty()
&& !_form->details().termsAccepted) {
_panel->requestTermsAcceptance(
_form->details().termsBotUsername,
invoice.termsUrl,
invoice.isRecurring);
} else {
RegisterPaymentStart(this, { _form->invoice().cover.title });
_submitState = SubmitState::Finishing;
_form->submit();
}
}
void CheckoutProcess::panelTrustAndSubmit() {
_form->trustBot();
panelSubmit();
}
void CheckoutProcess::panelAcceptTermsAndSubmit() {
_form->acceptTerms();
panelSubmit();
}
void CheckoutProcess::panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) {
if (!message.isArray()) {
LOG(("Payments Error: "
"Not an array received in buy_callback arguments."));
return;
}
const auto list = message.array();
if (list.at(0).toString() != "payment_form_submit") {
return;
} else if (!list.at(1).isString()) {
LOG(("Payments Error: "
"Not a string received in buy_callback result."));
return;
}
auto error = QJsonParseError();
const auto document = QJsonDocument::fromJson(
list.at(1).toString().toUtf8(),
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payments Error: "
"Failed to parse buy_callback arguments, error: %1."
).arg(error.errorString()));
return;
} else if (!document.isObject()) {
LOG(("Payments Error: "
"Not an object decoded in buy_callback result."));
return;
}
const auto root = document.object();
const auto title = root.value("title").toString();
const auto credentials = root.value("credentials");
if (!credentials.isObject()) {
LOG(("Payments Error: "
"Not an object received in payment credentials."));
return;
}
crl::on_main(this, [=] {
_form->setPaymentCredentials(NewCredentials{
.title = title,
.data = QJsonDocument(
credentials.toObject()
).toJson(QJsonDocument::Compact),
.saveOnServer = saveInformation,
});
});
}
std::optional<QDate> CheckoutProcess::panelOverrideExpireDateThreshold() {
return _form->overrideExpireDateThreshold();
}
bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {
if (Core::TryConvertUrlToLocal(uri) == uri) {
return true;
}
// #TODO payments
crl::on_main(this, [=] { closeAndReactivate(CheckoutResult::Paid); });
return false;
}
void CheckoutProcess::panelCancelEdit() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
showForm();
}
void CheckoutProcess::panelEditPaymentMethod() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
editPaymentMethod();
}
void CheckoutProcess::panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) {
_form->validateCard(data, saveInformation);
}
void CheckoutProcess::panelEditShippingInformation() {
showEditInformation(Ui::InformationField::ShippingStreet);
}
void CheckoutProcess::panelEditName() {
showEditInformation(Ui::InformationField::Name);
}
void CheckoutProcess::panelEditEmail() {
showEditInformation(Ui::InformationField::Email);
}
void CheckoutProcess::panelEditPhone() {
showEditInformation(Ui::InformationField::Phone);
}
void CheckoutProcess::showForm() {
_panel->showForm(
_form->invoice(),
_form->information(),
_form->paymentMethod().ui,
_form->shippingOptions());
if (_nonPanelPaymentFormProcess && !_realFormNotified) {
_realFormNotified = true;
const auto weak = base::make_weak(_panel.get());
_nonPanelPaymentFormProcess(RealFormPresentedNotification());
if (weak) {
requestActivate();
}
}
}
void CheckoutProcess::showEditInformation(Ui::InformationField field) {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
_panel->showEditInformation(
_form->invoice(),
_form->information(),
field);
}
void CheckoutProcess::showInformationError(Ui::InformationField field) {
Expects(_submitState != SubmitState::Validated);
if (_submitState != SubmitState::None) {
return;
}
_panel->showInformationError(
_form->invoice(),
_form->information(),
field);
}
void CheckoutProcess::showCardError(Ui::CardField field) {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
_panel->showCardError(_form->paymentMethod().ui.native, field);
}
void CheckoutProcess::chooseShippingOption() {
_panel->chooseShippingOption(_form->shippingOptions());
}
void CheckoutProcess::chooseTips() {
_panel->chooseTips(_form->invoice());
}
void CheckoutProcess::editPaymentMethod() {
_panel->choosePaymentMethod(_form->paymentMethod().ui);
}
void CheckoutProcess::requestSetPassword() {
_session->api().cloudPassword().reload();
_panel->askSetPassword();
}
void CheckoutProcess::requestPassword() {
getPasswordState([=](const Core::CloudPasswordState &state) {
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_payments_password_title();
const auto &method = _form->paymentMethod();
const auto &list = method.savedCredentials;
const auto index = method.savedCredentialsIndex;
fields.customDescription = tr::lng_payments_password_description(
tr::now,
lt_card,
(index < list.size()) ? list[index].title : QString());
fields.customSubmitButton = tr::lng_payments_password_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> box) {
_enterPasswordBox = box;
_form->submit(result);
};
_panel->showBox(Box<PasscodeBox>(_session, fields));
});
}
void CheckoutProcess::panelSetPassword() {
getPasswordState([=](const Core::CloudPasswordState &state) {
if (state.hasPassword) {
return;
}
auto owned = Box<PasscodeBox>(
_session,
PasscodeBox::CloudFields::From(state));
const auto box = owned.data();
rpl::merge(
box->newPasswordSet() | rpl::to_empty,
box->passwordReloadNeeded()
) | rpl::on_next([=] {
_session->api().cloudPassword().reload();
}, box->lifetime());
box->clearUnconfirmedPassword(
) | rpl::on_next([=] {
_session->api().cloudPassword().clearUnconfirmedPassword();
}, box->lifetime());
_panel->showBox(std::move(owned));
});
}
void CheckoutProcess::panelOpenUrl(const QString &url) {
File::OpenUrl(url);
}
void CheckoutProcess::getPasswordState(
Fn<void(const Core::CloudPasswordState&)> callback) {
Expects(callback != nullptr);
if (_gettingPasswordState) {
return;
}
_session->api().cloudPassword().state(
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
_gettingPasswordState.destroy();
callback(state);
}, _gettingPasswordState);
}
void CheckoutProcess::panelChooseShippingOption() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
chooseShippingOption();
}
void CheckoutProcess::panelChangeShippingOption(const QString &id) {
_form->setShippingOption(id);
showForm();
}
void CheckoutProcess::panelChooseTips() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
chooseTips();
}
void CheckoutProcess::panelChangeTips(int64 value) {
_form->setTips(value);
showForm();
}
void CheckoutProcess::panelValidateInformation(
Ui::RequestedInformation data) {
if (_submitState == SubmitState::Validated) {
_submitState = SubmitState::None;
}
_form->validateInformation(data);
}
void CheckoutProcess::panelShowBox(object_ptr<Ui::BoxContent> box) {
_panel->showBox(std::move(box));
}
QVariant CheckoutProcess::panelClickHandlerContext() {
return QVariant::fromValue(ClickHandlerContext{
.show = _panel->uiShow(),
});
}
void CheckoutProcess::performInitialSilentValidation() {
const auto &invoice = _form->invoice();
const auto &saved = _form->information();
if (invoice.receipt
|| (invoice.isNameRequested && saved.name.isEmpty())
|| (invoice.isEmailRequested && saved.email.isEmpty())
|| (invoice.isPhoneRequested && saved.phone.isEmpty())
|| (invoice.isShippingAddressRequested && !saved.shippingAddress)) {
return;
}
_initialSilentValidation = true;
_form->validateInformation(saved);
}
Webview::StorageId CheckoutProcess::panelWebviewStorageId() {
return _session->local().resolveStorageIdOther();
}
Webview::ThemeParams CheckoutProcess::panelWebviewThemeParams() {
return Window::Theme::WebViewParams();
}
} // namespace Payments

View File

@@ -0,0 +1,206 @@
/*
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/weak_ptr.h"
#include "payments/ui/payments_panel_delegate.h"
#include "webview/webview_common.h"
class HistoryItem;
class PasscodeBox;
namespace Core {
struct CloudPasswordState;
} // namespace Core
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Payments::Ui {
class Panel;
enum class InformationField;
enum class CardField;
} // namespace Payments::Ui
namespace Payments {
class Form;
struct FormUpdate;
struct Error;
struct InvoiceCredits;
struct InvoiceStarGift;
struct InvoiceId;
struct InvoicePremiumGiftCode;
struct CreditsFormData;
struct CreditsReceiptData;
enum class Mode {
Payment,
Receipt,
};
enum class CheckoutResult {
Paid,
Pending,
Cancelled,
Failed,
Free, // Gift transfer attempt that doesn't need any payment.
};
struct RealFormPresentedNotification {
};
struct NonPanelPaymentForm
: std::variant<
std::shared_ptr<CreditsFormData>,
std::shared_ptr<CreditsReceiptData>,
RealFormPresentedNotification> {
using variant::variant;
};
struct PaidInvoice {
QString title;
};
class CheckoutProcess final
: public base::has_weak_ptr
, private Ui::PanelDelegate {
struct PrivateTag {};
public:
static void Start(
not_null<const HistoryItem*> item,
Mode mode,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
static void Start(
not_null<Main::Session*> session,
const QString &slug,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
static void Start(
InvoicePremiumGiftCode giftCodeInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess = nullptr);
static void Start(
InvoiceCredits creditsInvoice,
Fn<void(CheckoutResult)> reactivate);
static void Start(
InvoiceStarGift giftInvoice,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(
not_null<const HistoryItem*> item);
[[nodiscard]] static std::optional<PaidInvoice> InvoicePaid(
not_null<Main::Session*> session,
const QString &slug);
static void ClearAll();
CheckoutProcess(
InvoiceId id,
Mode mode,
Fn<void(CheckoutResult)> reactivate,
Fn<void(NonPanelPaymentForm)> nonPanelPaymentFormProcess,
PrivateTag);
~CheckoutProcess();
private:
enum class SubmitState {
None,
Validating,
Validated,
Finishing,
};
[[nodiscard]] not_null<PanelDelegate*> panelDelegate();
static void RegisterPaymentStart(
not_null<CheckoutProcess*> process,
PaidInvoice info);
static void UnregisterPaymentStart(not_null<CheckoutProcess*> process);
void setReactivateCallback(Fn<void(CheckoutResult)> reactivate);
void setNonPanelPaymentFormProcess(Fn<void(NonPanelPaymentForm)>);
void requestActivate();
void closeAndReactivate(CheckoutResult result);
void close();
void handleFormUpdate(const FormUpdate &update);
void handleError(const Error &error);
void showForm();
void showEditInformation(Ui::InformationField field);
void showInformationError(Ui::InformationField field);
void showCardError(Ui::CardField field);
void chooseShippingOption();
void chooseTips();
void editPaymentMethod();
void requestSetPassword();
void requestPassword();
void getPasswordState(
Fn<void(const Core::CloudPasswordState&)> callback);
void performInitialSilentValidation();
void panelRequestClose() override;
void panelCloseSure() override;
void panelSubmit() override;
void panelTrustAndSubmit() override;
void panelAcceptTermsAndSubmit() override;
void panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelSetPassword() override;
void panelOpenUrl(const QString &url) override;
void panelCancelEdit() override;
void panelEditPaymentMethod() override;
void panelEditShippingInformation() override;
void panelEditName() override;
void panelEditEmail() override;
void panelEditPhone() override;
void panelChooseShippingOption() override;
void panelChangeShippingOption(const QString &id) override;
void panelChooseTips() override;
void panelChangeTips(int64 value) override;
void panelValidateInformation(Ui::RequestedInformation data) override;
void panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) override;
void panelShowBox(object_ptr<Ui::BoxContent> box) override;
QVariant panelClickHandlerContext() override;
Webview::StorageId panelWebviewStorageId() override;
Webview::ThemeParams panelWebviewThemeParams() override;
std::optional<QDate> panelOverrideExpireDateThreshold() override;
const not_null<Main::Session*> _session;
const std::unique_ptr<Form> _form;
const std::unique_ptr<Ui::Panel> _panel;
base::weak_qptr<PasscodeBox> _enterPasswordBox;
Fn<void(CheckoutResult)> _reactivate;
Fn<void(NonPanelPaymentForm)> _nonPanelPaymentFormProcess;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;
bool _realFormNotified = false;
bool _sendFormPending = false;
bool _sendFormFailed = false;
rpl::lifetime _gettingPasswordState;
rpl::lifetime _lifetime;
};
} // namespace Payments

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,423 @@
/*
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 "payments/ui/payments_panel_data.h"
#include "base/weak_ptr.h"
#include "mtproto/sender.h"
class Image;
class QJsonObject;
namespace Core {
struct CloudPasswordResult;
} // namespace Core
namespace Stripe {
class APIClient;
} // namespace Stripe
namespace SmartGlocal {
class APIClient;
} // namespace SmartGlocal
namespace Main {
class Session;
} // namespace Main
namespace Data {
class PhotoMedia;
} // namespace Data
namespace Payments {
enum class Mode;
struct FormDetails {
uint64 formId = 0;
QString url;
QString nativeProvider;
QString termsBotUsername;
QByteArray nativeParamsJson;
UserId botId = 0;
UserId providerId = 0;
bool canSaveCredentials = false;
bool passwordMissing = false;
bool termsAccepted = false;
[[nodiscard]] bool valid() const {
return !url.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct ThumbnailLoadProcess {
std::shared_ptr<Data::PhotoMedia> view;
bool blurredSet = false;
rpl::lifetime lifetime;
};
struct SavedCredentials {
QString id;
QString title;
[[nodiscard]] bool valid() const {
return !id.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct NewCredentials {
QString title;
QByteArray data;
bool saveOnServer = false;
[[nodiscard]] bool empty() const {
return data.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct StripePaymentMethod {
QString publishableKey;
};
struct SmartGlocalPaymentMethod {
QString publicToken;
QString tokenizeUrl;
};
struct NativePaymentMethod {
std::variant<
v::null_t,
StripePaymentMethod,
SmartGlocalPaymentMethod> data;
[[nodiscard]] bool valid() const {
return !v::is_null(data);
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct PaymentMethod {
NativePaymentMethod native;
std::vector<SavedCredentials> savedCredentials;
int savedCredentialsIndex = 0;
NewCredentials newCredentials;
Ui::PaymentMethodDetails ui;
};
struct InvoiceMessage {
not_null<PeerData*> peer;
MsgId itemId = 0;
};
struct InvoiceSlug {
not_null<Main::Session*> session;
QString slug;
};
struct InvoicePremiumGiftCodeGiveaway {
not_null<ChannelData*> boostPeer;
std::vector<not_null<ChannelData*>> additionalChannels;
std::vector<QString> countries;
QString additionalPrize;
TimeId untilDate = 0;
bool onlyNewSubscribers = false;
bool showWinners = false;
};
struct InvoicePremiumGiftCodeUsers {
std::vector<not_null<UserData*>> users;
ChannelData *boostPeer = nullptr;
TextWithEntities message;
};
struct InvoicePremiumGiftCode {
std::variant<
InvoicePremiumGiftCodeUsers,
InvoicePremiumGiftCodeGiveaway> purpose;
QString currency;
QString storeProduct;
std::optional<uint64> giveawayCredits;
uint64 randomId = 0;
uint64 amount = 0;
int storeQuantity = 0;
int users = 0;
int months = 0;
};
struct InvoiceCredits {
not_null<Main::Session*> session;
uint64 randomId = 0;
uint64 credits = 0;
QString product;
QString currency;
uint64 amount = 0;
bool extended = false;
PeerId giftPeerId = PeerId(0);
int subscriptionPeriod = 0;
};
struct InvoiceStarGift {
uint64 giftId = 0;
uint64 randomId = 0;
TextWithEntities message;
not_null<PeerData*> recipient;
int limitedCount = 0;
int perUserLimit = 0;
bool anonymous = false;
bool upgraded = false;
};
struct InvoiceId {
std::variant<
InvoiceMessage,
InvoiceSlug,
InvoicePremiumGiftCode,
InvoiceCredits,
InvoiceStarGift> value;
};
struct CreditsFormData {
InvoiceId id;
uint64 formId = 0;
uint64 botId = 0;
QString title;
QString description;
PhotoData *photo = nullptr;
InvoiceCredits invoice;
MTPInputInvoice inputInvoice;
int starGiftLimitedCount = 0;
int starGiftPerUserLimit = 0;
bool starGiftForm = false;
};
struct CreditsReceiptData {
QString id;
QString title;
QString description;
PhotoData *photo = nullptr;
PeerId peerId = PeerId(0);
CreditsAmount credits;
TimeId date = 0;
};
struct ToggleProgress {
bool shown = true;
};
struct FormReady {};
struct ThumbnailUpdated {
QImage thumbnail;
};
struct ValidateFinished {};
struct PaymentMethodUpdate {
bool requestNewPassword = false;
};
struct VerificationNeeded {
QString url;
};
struct TmpPasswordRequired {};
struct BotTrustRequired {
not_null<UserData*> bot;
not_null<UserData*> provider;
};
struct PaymentFinished {
MTPUpdates updates;
};
struct CreditsPaymentStarted {
CreditsFormData data;
};
struct CreditsReceiptReady {
CreditsReceiptData data;
};
struct Error {
enum class Type {
None,
Form,
Validate,
Stripe,
SmartGlocal,
TmpPassword,
Send,
};
Type type = Type::None;
QString id;
[[nodiscard]] bool empty() const {
return (type == Type::None);
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct FormUpdate : std::variant<
ToggleProgress,
FormReady,
ThumbnailUpdated,
ValidateFinished,
PaymentMethodUpdate,
VerificationNeeded,
TmpPasswordRequired,
BotTrustRequired,
PaymentFinished,
CreditsPaymentStarted,
CreditsReceiptReady,
Error> {
using variant::variant;
};
[[nodiscard]] not_null<Main::Session*> SessionFromId(const InvoiceId &id);
[[nodiscard]] MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL(
const InvoicePremiumGiftCode &invoice);
[[nodiscard]] MTPinputStorePaymentPurpose InvoiceCreditsGiveawayToTL(
const InvoicePremiumGiftCode &invoice);
[[nodiscard]] bool IsPremiumForStarsInvoice(const InvoiceId &id);
class Form final : public base::has_weak_ptr {
public:
Form(InvoiceId id, bool receipt);
~Form();
[[nodiscard]] const Ui::Invoice &invoice() const {
return _invoice;
}
[[nodiscard]] const FormDetails &details() const {
return _details;
}
[[nodiscard]] const Ui::RequestedInformation &information() const {
return _information;
}
[[nodiscard]] const PaymentMethod &paymentMethod() const {
return _paymentMethod;
}
[[nodiscard]] const Ui::ShippingOptions &shippingOptions() const {
return _shippingOptions;
}
[[nodiscard]] bool hasChanges() const;
[[nodiscard]] rpl::producer<FormUpdate> updates() const {
return _updates.events();
}
[[nodiscard]] std::optional<QDate> overrideExpireDateThreshold() const;
void validateInformation(const Ui::RequestedInformation &information);
void validateCard(
const Ui::UncheckedCardDetails &details,
bool saveInformation);
void setPaymentCredentials(const NewCredentials &credentials);
void chooseSavedMethod(const QString &id);
void setHasPassword(bool has);
void setShippingOption(const QString &id);
void setTips(int64 value);
void acceptTerms();
void trustBot();
void submit();
void submit(const Core::CloudPasswordResult &result);
private:
void fillInvoiceFromMessage();
void showProgress();
void hideProgress();
[[nodiscard]] Data::FileOrigin thumbnailFileOrigin() const;
void loadThumbnail(not_null<PhotoData*> photo);
[[nodiscard]] QImage prepareGoodThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareBlurredThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareThumbnail(
not_null<const Image*> image,
bool blurred = false) const;
[[nodiscard]] QImage prepareEmptyThumbnail() const;
void requestForm();
void requestReceipt();
void processForm(const MTPDpayments_paymentForm &data);
void processReceipt(const MTPDpayments_paymentReceipt &data);
void processReceipt(const MTPDpayments_paymentReceiptStars &data);
void processInvoice(const MTPDinvoice &data);
void processDetails(const MTPDpayments_paymentForm &data);
void processDetails(const MTPDpayments_paymentReceipt &data);
void processDetails(const MTPDpayments_paymentReceiptStars &data);
void processSavedInformation(const MTPDpaymentRequestedInfo &data);
void processAdditionalPaymentMethods(
const QVector<MTPPaymentFormMethod> &list);
void processShippingOptions(const QVector<MTPShippingOption> &data);
void fillPaymentMethodInformation();
void fillStripeNativeMethod(QJsonObject object);
void fillSmartGlocalNativeMethod(QJsonObject object);
void refreshPaymentMethodDetails();
void refreshSavedPaymentMethodDetails();
[[nodiscard]] QString defaultPhone() const;
[[nodiscard]] QString defaultCountry() const;
[[nodiscard]] MTPInputInvoice inputInvoice() const;
void validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details,
bool saveInformation);
void validateCard(
const SmartGlocalPaymentMethod &method,
const Ui::UncheckedCardDetails &details,
bool saveInformation);
bool validateInformationLocal(
const Ui::RequestedInformation &information) const;
[[nodiscard]] Error informationErrorLocal(
const Ui::RequestedInformation &information) const;
bool validateCardLocal(
const Ui::UncheckedCardDetails &details,
const std::optional<QDate> &overrideExpireDateThreshold) const;
[[nodiscard]] Error cardErrorLocal(
const Ui::UncheckedCardDetails &details,
const std::optional<QDate> &overrideExpireDateThreshold) const;
const InvoiceId _id;
const not_null<Main::Session*> _session;
MTP::Sender _api;
bool _receiptMode = false;
Ui::Invoice _invoice;
std::unique_ptr<ThumbnailLoadProcess> _thumbnailLoadProcess;
FormDetails _details;
Ui::RequestedInformation _savedInformation;
Ui::RequestedInformation _information;
PaymentMethod _paymentMethod;
Ui::RequestedInformation _validatedInformation;
mtpRequestId _validateRequestId = 0;
mtpRequestId _passwordRequestId = 0;
std::unique_ptr<Stripe::APIClient> _stripe;
std::unique_ptr<SmartGlocal::APIClient> _smartglocal;
Ui::ShippingOptions _shippingOptions;
QString _requestedInformationId;
rpl::event_stream<FormUpdate> _updates;
};
} // namespace Payments

View File

@@ -0,0 +1,184 @@
/*
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/payments_non_panel_process.h"
#include "api/api_credits.h"
#include "base/unixtime.h"
#include "boxes/send_credits_box.h"
#include "data/components/credits.h"
#include "data/data_credits.h"
#include "data/data_photo.h"
#include "data/data_user.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "payments/payments_checkout_process.h" // NonPanelPaymentForm.
#include "payments/payments_form.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/boost_box.h" // Ui::StartFireworks.
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
namespace Payments {
bool IsCreditsInvoice(not_null<HistoryItem*> item) {
if (const auto payment = item->Get<HistoryServicePayment>()) {
return payment->isCreditsCurrency;
}
const auto media = item->media();
const auto invoice = media ? media->invoice() : nullptr;
return invoice && (invoice->currency == Ui::kCreditsCurrency);
}
void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Blocked) {
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Failed);
}
return;
} else if (result == Settings::SmallBalanceResult::Cancelled) {
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Cancelled);
}
return;
} else if (form->starGiftForm
|| IsPremiumForStarsInvoice(form->id)) {
const auto done = [=](std::optional<QString> error) {
const auto onstack = maybeReturnToBot;
if (error) {
if (*error == u"STARGIFT_USAGE_LIMITED"_q) {
if (form->starGiftLimitedCount) {
show->showToast({
.title = tr::lng_gift_sold_out_title(
tr::now),
.text = tr::lng_gift_sold_out_text(
tr::now,
lt_count_decimal,
form->starGiftLimitedCount,
tr::rich),
});
} else {
show->showToast(
tr::lng_gift_sold_out_title(tr::now));
}
} else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) {
show->showToast({
.text = tr::lng_gift_sent_finished(
tr::now,
lt_count,
std::max(form->starGiftPerUserLimit, 1),
tr::rich),
});
} else {
show->showToast(*error);
}
if (onstack) {
onstack(CheckoutResult::Failed);
}
} else if (onstack) {
onstack(CheckoutResult::Paid);
}
};
Ui::SendStarsForm(&show->session(), form, done);
return;
}
const auto unsuccessful = std::make_shared<bool>(true);
const auto box = show->show(Box(
Ui::SendCreditsBox,
form,
[=] {
*unsuccessful = false;
if (const auto widget = fireworks.data()) {
Ui::StartFireworks(widget);
}
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Paid);
}
}));
box->boxClosing() | rpl::on_next([=] {
crl::on_main([=] {
if (*unsuccessful) {
if (const auto onstack = maybeReturnToBot) {
onstack(CheckoutResult::Cancelled);
}
}
});
}, box->lifetime());
};
using namespace Settings;
const auto starGift = std::get_if<InvoiceStarGift>(&form->id.value);
auto source = !starGift
? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId })
: SmallBalanceSource(SmallBalanceStarGift{
.recipientId = starGift->recipient->id,
});
MaybeRequestBalanceIncrease(show, form->invoice.credits, source, done);
}
void ProcessCreditsReceipt(
not_null<Window::SessionController*> controller,
std::shared_ptr<CreditsReceiptData> receipt,
Fn<void(CheckoutResult)> maybeReturnToBot) {
const auto entry = Data::CreditsHistoryEntry{
.id = receipt->id,
.title = receipt->title,
.description = { receipt->description },
.date = base::unixtime::parse(receipt->date),
.photoId = receipt->photo ? receipt->photo->id : 0,
.credits = receipt->credits,
.bareMsgId = uint64(),
.barePeerId = receipt->peerId.value,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
};
controller->uiShow()->show(Box(
Settings::ReceiptCreditsBox,
controller,
entry,
Data::SubscriptionEntry{}));
controller->window().activate();
}
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
Fn<void(CheckoutResult)> maybeReturnToBot) {
return [=](NonPanelPaymentForm form) {
using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
v::match(form, [&](const CreditsFormDataPtr &form) {
ProcessCreditsPayment(
controller->uiShow(),
controller->content().get(),
form,
maybeReturnToBot);
controller->window().activate();
}, [&](const CreditsReceiptPtr &receipt) {
ProcessCreditsReceipt(controller, receipt, maybeReturnToBot);
}, [](RealFormPresentedNotification) {});
};
}
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
return IsCreditsInvoice(item)
? ProcessNonPanelPaymentFormFactory(controller)
: nullptr;
}
} // namespace Payments

View File

@@ -0,0 +1,48 @@
/*
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
class HistoryItem;
namespace Main {
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Payments {
enum class CheckoutResult;
struct CreditsFormData;
struct CreditsReceiptData;
struct NonPanelPaymentForm;
[[nodiscard]] bool IsCreditsInvoice(not_null<HistoryItem*> item);
void ProcessCreditsPayment(
std::shared_ptr<Main::SessionShow> show,
QPointer<QWidget> fireworks,
std::shared_ptr<CreditsFormData> form,
Fn<void(CheckoutResult)> maybeReturnToBot = nullptr);
void ProcessCreditsReceipt(
not_null<Window::SessionController*> controller,
std::shared_ptr<CreditsReceiptData> receipt,
Fn<void(CheckoutResult)> maybeReturnToBot = nullptr);
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
Fn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr);
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
} // namespace Payments

View File

@@ -0,0 +1,309 @@
/*
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/payments_reaction_process.h"
#include "api/api_credits.h"
#include "api/api_global_privacy.h"
#include "apiwrap.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "boxes/send_credits_box.h" // CreditsEmojiSmall.
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_messages.h"
#include "core/ui_integration.h" // TextContext.
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/session/session_show.h"
#include "main/session/send_as_peers.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "payments/ui/payments_reaction_box.h"
#include "settings/settings_credits_graphics.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/layers/box_content.h"
#include "ui/layers/generic_box.h"
#include "ui/layers/show.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
#include "window/window_session_controller.h"
namespace Payments {
namespace {
constexpr auto kMaxPerReactionFallback = 10'000;
constexpr auto kDefaultPerReaction = 50;
void TryAddingPaidReaction(
std::shared_ptr<Main::SessionShow> show,
FullMsgId itemId,
base::weak_ptr<HistoryView::Element> weakView,
int count,
std::optional<PeerId> shownPeer,
Fn<void(bool)> finished) {
const auto owner = &show->session().data();
const auto checkItem = [=] {
const auto item = owner->message(itemId);
if (!item) {
if (const auto onstack = finished) {
onstack(false);
}
}
return item;
};
const auto item = checkItem();
if (!item) {
return;
}
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
if (const auto item = checkItem()) {
item->addPaidReaction(count, shownPeer);
if (const auto view = count ? weakView.get() : nullptr) {
const auto history = view->history();
history->owner().notifyViewPaidReactionSent(view);
view->animateReaction({
.id = Data::ReactionId::Paid(),
});
}
if (const auto onstack = finished) {
onstack(true);
}
}
} else if (const auto onstack = finished) {
onstack(false);
}
};
const auto channelId = peerToChannel(itemId.peer);
Settings::MaybeRequestBalanceIncrease(
show,
count,
Settings::SmallBalanceReaction{ .channelId = channelId },
done);
}
} // namespace
bool LookupMyPaidAnonymous(not_null<HistoryItem*> item) {
for (const auto &entry : item->topPaidReactionsWithLocal()) {
if (entry.my) {
return !entry.peer;
}
}
return false;
}
void TryAddingPaidReaction(
not_null<HistoryItem*> item,
HistoryView::Element *view,
int count,
std::optional<PeerId> shownPeer,
std::shared_ptr<Main::SessionShow> show,
Fn<void(bool)> finished) {
TryAddingPaidReaction(
std::move(show),
item->fullId(),
view,
count,
shownPeer,
std::move(finished));
}
void TryAddingPaidReaction(
not_null<Calls::GroupCall*> call,
int count,
std::shared_ptr<Main::SessionShow> show,
Fn<void(bool)> finished) {
const auto checkCall = [weak = base::make_weak(call), finished] {
const auto strong = weak.get();
if (!strong) {
if (const auto onstack = finished) {
onstack(false);
}
}
return strong;
};
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
if (const auto call = checkCall()) {
call->messages()->reactionsPaidAdd(count);
call->peer()->owner().notifyCallPaidReactionSent(call);
if (const auto onstack = finished) {
onstack(true);
}
}
} else if (const auto onstack = finished) {
onstack(false);
}
};
Settings::MaybeRequestBalanceIncrease(
show,
count,
Settings::SmallBalanceVideoStream{ .streamerId = call->peer()->id },
done);
}
void ShowPaidReactionDetails(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
HistoryView::Element *view,
HistoryReactionSource source) {
Expects(item->history()->peer->isBroadcast()
|| item->isDiscussionPost());
const auto show = controller->uiShow();
const auto itemId = item->fullId();
const auto session = &item->history()->session();
const auto appConfig = &session->appConfig();
const auto max = std::max(
appConfig->get<int>(
u"stars_paid_reaction_amount_max"_q,
kMaxPerReactionFallback),
2);
const auto chosen = std::clamp(kDefaultPerReaction, 1, max);
struct State {
base::weak_qptr<Ui::BoxContent> selectBox;
bool ignoreShownPeerSwitch = false;
bool sending = false;
};
const auto state = std::make_shared<State>();
session->credits().load(true);
const auto weakView = base::make_weak(view);
const auto send = [=](int count, PeerId shownPeer, auto resend) -> void {
Expects(count >= 0);
const auto finish = [=](bool success) {
state->sending = false;
if (success && count > 0) {
state->ignoreShownPeerSwitch = true;
if (const auto strong = state->selectBox.get()) {
strong->closeBox();
}
}
};
if (state->sending || (!count && state->ignoreShownPeerSwitch)) {
return;
} else if (const auto item = session->data().message(itemId)) {
state->sending = true;
TryAddingPaidReaction(
item,
weakView.get(),
count,
shownPeer,
show,
finish);
}
};
auto submitText = [=](rpl::producer<int> amount) {
auto nice = std::move(amount) | rpl::map([=](int count) {
return Ui::CreditsEmojiSmall().append(
Lang::FormatCountDecimal(count));
});
return tr::lng_paid_react_send(
lt_price,
std::move(nice),
tr::rich);
};
auto top = std::vector<Ui::PaidReactionTop>();
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
const auto peer = entry.peer;
const auto name = peer
? peer->shortName()
: tr::lng_paid_react_anonymous(tr::now);
const auto open = [=] {
controller->uiShow()->show(PrepareShortInfoBox(peer, controller));
};
top.push_back({
.name = name,
.photo = (peer
? Ui::MakeUserpicThumbnail(peer)
: Ui::MakeHiddenAuthorThumbnail()),
.barePeerId = peer ? uint64(peer->id.value) : 0,
.count = int(entry.count),
.click = peer ? open : Fn<void()>(),
.my = (entry.my == 1),
});
};
const auto linked = item->discussionPostOriginalSender();
const auto channel = (linked ? linked : item->history()->peer.get());
const auto channels = session->sendAsPeers().list(
{ channel, Main::SendAsType::PaidReaction }
) | ranges::views::transform(
&Main::SendAsPeer::peer
) | ranges::to_vector;
const auto topPaid = item->topPaidReactionsWithLocal();
top.reserve(topPaid.size() + 2 + channels.size());
for (const auto &entry : topPaid) {
add(entry);
}
auto myAdded = base::flat_set<uint64>();
const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);
if (i != end(top)) {
myAdded.emplace(i->barePeerId);
}
const auto myCount = uint32((i != end(top)) ? i->count : 0);
const auto myAdd = [&](PeerData *peer) {
const auto barePeerId = peer ? uint64(peer->id.value) : 0;
if (!myAdded.emplace(barePeerId).second) {
return;
}
add(Data::MessageReactionsTopPaid{
.peer = peer,
.count = myCount,
.my = true,
});
};
const auto globalPrivacy = &session->api().globalPrivacy();
const auto shown = globalPrivacy->paidReactionShownPeerCurrent();
const auto owner = &session->data();
const auto shownPeer = shown ? owner->peer(shown).get() : nullptr;
myAdd(shownPeer);
myAdd(session->user());
myAdd(nullptr);
for (const auto &channel : channels) {
myAdd(channel);
}
ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
state->selectBox = show->show(Ui::MakePaidReactionBox({
.chosen = chosen,
.max = max,
.top = std::move(top),
.session = &channel->session(),
.name = channel->name(),
.submit = std::move(submitText),
.balanceValue = session->credits().balanceValue(),
.send = [=](int count, uint64 barePeerId) {
send(count, PeerId(barePeerId), send);
},
}));
if (const auto strong = state->selectBox.get()) {
session->data().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> removed) {
if (removed == item) {
strong->closeBox();
}
}, strong->lifetime());
}
}
} // namespace Payments

View File

@@ -0,0 +1,54 @@
/*
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
enum class HistoryReactionSource : char;
class HistoryItem;
namespace Calls {
class GroupCall;
} // namespace Calls
namespace HistoryView {
class Element;
} // namespace HistoryView
namespace Main {
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Payments {
[[nodiscard]] bool LookupMyPaidAnonymous(not_null<HistoryItem*> item);
void TryAddingPaidReaction(
not_null<HistoryItem*> item,
HistoryView::Element *view,
int count,
std::optional<PeerId> shownPeer,
std::shared_ptr<Main::SessionShow> show,
Fn<void(bool)> finished = nullptr);
void TryAddingPaidReaction(
not_null<Calls::GroupCall*> call,
int count,
std::shared_ptr<Main::SessionShow> show,
Fn<void(bool)> finished = nullptr);
void ShowPaidReactionDetails(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
HistoryView::Element *view,
HistoryReactionSource source);
} // namespace Payments

View File

@@ -0,0 +1,177 @@
/*
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 "smartglocal/smartglocal_api_client.h"
#include "smartglocal/smartglocal_error.h"
#include "smartglocal/smartglocal_token.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
namespace SmartGlocal {
namespace {
[[nodiscard]] QString APIURLBase(bool isTest) {
return isTest
? "tgb-playground.smart-glocal.com/cds/v1"
: "tgb.smart-glocal.com/cds/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokenize/card";
}
[[nodiscard]] QByteArray ToJson(const Stripe::CardParams &card) {
const auto zero = QChar('0');
const auto month = QString("%1").arg(card.expMonth, 2, 10, zero);
const auto year = QString("%1").arg(card.expYear % 100, 2, 10, zero);
return QJsonDocument(QJsonObject{
{ "card", QJsonObject{
{ "number", card.number },
{ "expiration_month", month },
{ "expiration_year", year },
{ "security_code", card.cvc },
} },
}).toJson(QJsonDocument::Compact);
}
[[nodiscard]] QString ComputeApiUrl(PaymentConfiguration configuration) {
const auto url = configuration.tokenizeUrl;
if (url.startsWith("https://")
&& url.endsWith(".smart-glocal.com/cds/v1/tokenize/card")) {
return url;
}
return QString("https://%1/%2")
.arg(APIURLBase(configuration.isTest))
.arg(TokenEndpoint());
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl(ComputeApiUrl(configuration))
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-PUBLIC-TOKEN", _configuration.publicToken },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
Stripe::CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(ToJson(card), std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl);
auto request = QNetworkRequest(url);
request.setHeader(
QNetworkRequest::ContentTypeHeader,
"application/json");
for (const auto &[name, value] : _additionalHttpHeaders) {
request.setRawHeader(name.toUtf8(), value.toUtf8());
}
destroyReplyDelayed(std::move(_reply));
_reply.reset(_manager.post(request, data));
const auto finish = [=](Token token, Error error) {
crl::on_main([
completion,
token = std::move(token),
error = std::move(error)
] {
completion(std::move(token), std::move(error));
});
};
const auto finishWithError = [=](Error error) {
finish(Token::Empty(), std::move(error));
};
const auto finishWithToken = [=](Token token) {
finish(std::move(token), Error::None());
};
QObject::connect(_reply.get(), &QNetworkReply::finished, [=] {
const auto replyError = int(_reply->error());
const auto replyErrorString = _reply->errorString();
const auto bytes = _reply->readAll();
destroyReplyDelayed(std::move(_reply));
auto parseError = QJsonParseError();
const auto document = QJsonDocument::fromJson(bytes, &parseError);
if (!bytes.isEmpty()) {
if (parseError.error != QJsonParseError::NoError) {
const auto code = int(parseError.error);
finishWithError({
Error::Code::JsonParse,
QString("InvalidJson%1").arg(code),
parseError.errorString(),
});
return;
} else if (!document.isObject()) {
finishWithError({
Error::Code::JsonFormat,
"InvalidJsonRoot",
"Not an object in JSON reply.",
});
return;
}
const auto object = document.object();
if (auto error = Error::DecodedObjectFromResponse(object)) {
finishWithError(std::move(error));
return;
}
}
if (replyError != QNetworkReply::NoError) {
finishWithError({
Error::Code::Network,
QString("RequestError%1").arg(replyError),
replyErrorString,
});
return;
}
auto token = Token::DecodedObjectFromAPIResponse(
document.object().value("data").toObject());
if (!token) {
finishWithError({
Error::Code::JsonFormat,
"InvalidTokenJson",
"Could not parse token.",
});
}
finishWithToken(std::move(token));
});
}
void APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {
if (!reply) {
return;
}
const auto raw = reply.get();
_old.push_back(std::move(reply));
QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);
raw->deleteLater();
QObject::connect(raw, &QObject::destroyed, [=] {
for (auto i = begin(_old); i != end(_old); ++i) {
if (i->get() == raw) {
i->release();
_old.erase(i);
break;
}
}
});
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,50 @@
/*
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 "stripe/stripe_card_params.h"
#include "smartglocal/smartglocal_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
#include <memory>
namespace SmartGlocal {
struct PaymentConfiguration {
QString publicToken;
QString tokenizeUrl;
bool isTest = false;
};
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
Stripe::CardParams card,
TokenCompletionCallback completion);
void createTokenWithData(
QByteArray data,
TokenCompletionCallback completion);
private:
void destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace SmartGlocal

View File

@@ -0,0 +1,19 @@
/*
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 <functional>
namespace SmartGlocal {
class Token;
class Error;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace SmartGlocal

View File

@@ -0,0 +1,63 @@
/*
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 "smartglocal/smartglocal_card.h"
#include <QtCore/QRegularExpression>
namespace SmartGlocal {
Card::Card(
QString type,
QString network,
QString maskedNumber)
: _type(type)
, _network(network)
, _maskedNumber(maskedNumber) {
}
Card Card::Empty() {
return Card(QString(), QString(), QString());
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto type = string(u"card_type");
const auto network = string(u"card_network");
const auto maskedNumber = string(u"masked_card_number");
if (type.isEmpty() || maskedNumber.isEmpty()) {
return Card::Empty();
}
return Card(type, network, maskedNumber);
}
QString Card::type() const {
return _type;
}
QString Card::network() const {
return _network;
}
QString Card::maskedNumber() const {
return _maskedNumber;
}
bool Card::empty() const {
return _type.isEmpty() || _maskedNumber.isEmpty();
}
QString Last4(const Card &card) {
static const auto RegExp = QRegularExpression("[^\\d]\\d*(\\d{4})$");
const auto masked = card.maskedNumber();
const auto m = RegExp.match(masked);
return m.hasMatch() ? m.captured(1) : QString();
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,51 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace SmartGlocal {
class Card final {
public:
Card(const Card &other) = default;
Card &operator=(const Card &other) = default;
Card(Card &&other) = default;
Card &operator=(Card &&other) = default;
~Card() = default;
[[nodiscard]] static Card Empty();
[[nodiscard]] static Card DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] QString type() const;
[[nodiscard]] QString network() const;
[[nodiscard]] QString maskedNumber() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Card(
QString type,
QString network,
QString maskedNumber);
QString _type;
QString _network;
QString _maskedNumber;
};
[[nodiscard]] QString Last4(const Card &card);
} // namespace SmartGlocal

View File

@@ -0,0 +1,69 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "smartglocal/smartglocal_error.h"
namespace SmartGlocal {
Error::Code Error::code() const {
return _code;
}
QString Error::description() const {
return _description;
}
QString Error::message() const {
return _message;
}
QString Error::parameter() const {
return _parameter;
}
Error Error::None() {
return Error(Code::None, {}, {}, {});
}
Error Error::DecodedObjectFromResponse(QJsonObject object) {
if (object.value("status").toString() == "ok") {
return Error::None();
}
const auto entry = object.value("error");
if (!entry.isObject()) {
return {
Code::Unknown,
"GenericError",
"Could not read the error response "
"that was returned from SmartGlocal."
};
}
const auto error = entry.toObject();
const auto string = [&](QStringView key) {
return error.value(key).toString();
};
const auto code = string(u"code");
const auto description = string(u"description");
// There should always be a message and type for the error
if (code.isEmpty() || description.isEmpty()) {
return {
Code::Unknown,
"GenericError",
"Could not interpret the error response "
"that was returned from SmartGlocal."
};
}
return { Code::Unknown, code, description };
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,59 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace SmartGlocal {
class Error {
public:
enum class Code {
None = 0, // Non-SmartGlocal errors.
JsonParse = -1,
JsonFormat = -2,
Network = -3,
Unknown = 8,
};
Error(
Code code,
const QString &description,
const QString &message,
const QString &parameter = QString())
: _code(code)
, _description(description)
, _message(message)
, _parameter(parameter) {
}
[[nodiscard]] Code code() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString parameter() const;
[[nodiscard]] static Error None();
[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Code _code = Code::None;
QString _description;
QString _message;
QString _parameter;
};
} // namespace SmartGlocal

View File

@@ -0,0 +1,46 @@
/*
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 "smartglocal/smartglocal_token.h"
namespace SmartGlocal {
QString Token::tokenId() const {
return _tokenId;
}
Card Token::card() const {
return _card;
}
Token Token::Empty() {
return Token(QString());
}
Token Token::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto tokenId = object.value("token").toString();
if (tokenId.isEmpty()) {
return Token::Empty();
}
auto result = Token(tokenId);
const auto card = object.value("info");
if (card.isObject()) {
result._card = Card::DecodedObjectFromAPIResponse(card.toObject());
}
return result;
}
bool Token::empty() const {
return _tokenId.isEmpty();
}
Token::Token(QString tokenId)
: _tokenId(std::move(tokenId)) {
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,47 @@
/*
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 "smartglocal/smartglocal_card.h"
#include <QtCore/QDateTime>
class QJsonObject;
namespace SmartGlocal {
class Token {
public:
Token(const Token &other) = default;
Token &operator=(const Token &other) = default;
Token(Token &&other) = default;
Token &operator=(Token &&other) = default;
~Token() = default;
[[nodiscard]] QString tokenId() const;
[[nodiscard]] bool livemode() const;
[[nodiscard]] Card card() const;
[[nodiscard]] static Token Empty();
[[nodiscard]] static Token DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
explicit Token(QString tokenId);
QString _tokenId;
Card _card = Card::Empty();
};
} // namespace SmartGlocal

View File

@@ -0,0 +1,18 @@
/*
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
namespace Stripe {
enum class BillingAddressFields {
None,
Zip,
Full,
};
} // namespace Stripe

View File

@@ -0,0 +1,167 @@
/*
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 "stripe/stripe_api_client.h"
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
#include "stripe/stripe_form_encoder.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
namespace Stripe {
namespace {
[[nodiscard]] QString APIURLBase() {
return "api.stripe.com/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokens";
}
[[nodiscard]] QString StripeAPIVersion() {
return "2015-10-12";
}
[[nodiscard]] QString SDKVersion() {
return "9.1.0";
}
[[nodiscard]] QString StripeUserAgentDetails() {
const auto details = QJsonObject{
{ "lang", "objective-c" },
{ "bindings_version", SDKVersion() },
};
return QString::fromUtf8(
QJsonDocument(details).toJson(QJsonDocument::Compact));
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl("https://" + APIURLBase())
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-Stripe-User-Agent", StripeUserAgentDetails() },
{ "Stripe-Version", StripeAPIVersion() },
{ "Authorization", "Bearer " + _configuration.publishableKey },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(
FormEncoder::formEncodedDataForObject(MakeEncodable(card)),
std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl + '/' + TokenEndpoint());
auto request = QNetworkRequest(url);
for (const auto &[name, value] : _additionalHttpHeaders) {
request.setRawHeader(name.toUtf8(), value.toUtf8());
}
destroyReplyDelayed(std::move(_reply));
_reply.reset(_manager.post(request, data));
const auto finish = [=](Token token, Error error) {
crl::on_main([
completion,
token = std::move(token),
error = std::move(error)
] {
completion(std::move(token), std::move(error));
});
};
const auto finishWithError = [=](Error error) {
finish(Token::Empty(), std::move(error));
};
const auto finishWithToken = [=](Token token) {
finish(std::move(token), Error::None());
};
QObject::connect(_reply.get(), &QNetworkReply::finished, [=] {
const auto replyError = int(_reply->error());
const auto replyErrorString = _reply->errorString();
const auto bytes = _reply->readAll();
destroyReplyDelayed(std::move(_reply));
auto parseError = QJsonParseError();
const auto document = QJsonDocument::fromJson(bytes, &parseError);
if (!bytes.isEmpty()) {
if (parseError.error != QJsonParseError::NoError) {
const auto code = int(parseError.error);
finishWithError({
Error::Code::JsonParse,
QString("InvalidJson%1").arg(code),
parseError.errorString(),
});
return;
} else if (!document.isObject()) {
finishWithError({
Error::Code::JsonFormat,
"InvalidJsonRoot",
"Not an object in JSON reply.",
});
return;
}
const auto object = document.object();
if (auto error = Error::DecodedObjectFromResponse(object)) {
finishWithError(std::move(error));
return;
}
}
if (replyError != QNetworkReply::NoError) {
finishWithError({
Error::Code::Network,
QString("RequestError%1").arg(replyError),
replyErrorString,
});
return;
}
auto token = Token::DecodedObjectFromAPIResponse(document.object());
if (!token) {
finishWithError({
Error::Code::JsonFormat,
"InvalidTokenJson",
"Could not parse token.",
});
}
finishWithToken(std::move(token));
});
}
void APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {
if (!reply) {
return;
}
const auto raw = reply.get();
_old.push_back(std::move(reply));
QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);
raw->deleteLater();
QObject::connect(raw, &QObject::destroyed, [=] {
for (auto i = begin(_old); i != end(_old); ++i) {
if (i->get() == raw) {
i->release();
_old.erase(i);
break;
}
}
});
}
} // namespace Stripe

View File

@@ -0,0 +1,45 @@
/*
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 "stripe/stripe_payment_configuration.h"
#include "stripe/stripe_card_params.h"
#include "stripe/stripe_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
#include <memory>
namespace Stripe {
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
CardParams card,
TokenCompletionCallback completion);
void createTokenWithData(
QByteArray data,
TokenCompletionCallback completion);
private:
void destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace Stripe

View File

@@ -0,0 +1,19 @@
/*
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 <functional>
namespace Stripe {
class Error;
class Token;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace Stripe

View File

@@ -0,0 +1,188 @@
/*
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 "stripe/stripe_card.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
namespace {
CardBrand BrandFromString(const QString &brand) {
if (brand == "visa") {
return CardBrand::Visa;
} else if (brand == "american express") {
return CardBrand::Amex;
} else if (brand == "mastercard") {
return CardBrand::MasterCard;
} else if (brand == "discover") {
return CardBrand::Discover;
} else if (brand == "jcb") {
return CardBrand::JCB;
} else if (brand == "diners club") {
return CardBrand::DinersClub;
} else {
return CardBrand::Unknown;
}
}
CardFundingType FundingFromString(const QString &funding) {
if (funding == "credit") {
return CardFundingType::Credit;
} else if (funding == "debit") {
return CardFundingType::Debit;
} else if (funding == "prepaid") {
return CardFundingType::Prepaid;
} else {
return CardFundingType::Other;
}
}
} // namespace
Card::Card(
QString id,
QString last4,
CardBrand brand,
quint32 expMonth,
quint32 expYear)
: _cardId(id)
, _last4(last4)
, _brand(brand)
, _expMonth(expMonth)
, _expYear(expYear) {
}
Card Card::Empty() {
return Card(QString(), QString(), CardBrand::Unknown, 0, 0);
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
if (!ContainsFields(object, {
u"id",
u"last4",
u"brand",
u"exp_month",
u"exp_year"
})) {
return Card::Empty();
}
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto cardId = string(u"id");
const auto last4 = string(u"last4");
const auto brand = BrandFromString(string(u"brand").toLower());
const auto expMonth = object.value("exp_month").toInt();
const auto expYear = object.value("exp_year").toInt();
auto result = Card(cardId, last4, brand, expMonth, expYear);
result._name = string(u"name");
result._dynamicLast4 = string(u"dynamic_last4");
result._funding = FundingFromString(string(u"funding").toLower());
result._fingerprint = string(u"fingerprint");
result._country = string(u"country");
result._currency = string(u"currency");
result._addressLine1 = string(u"address_line1");
result._addressLine2 = string(u"address_line2");
result._addressCity = string(u"address_city");
result._addressState = string(u"address_state");
result._addressZip = string(u"address_zip");
result._addressCountry = string(u"address_country");
// TODO incomplete, not used.
//result._allResponseFields = object;
return result;
}
QString Card::cardId() const {
return _cardId;
}
QString Card::name() const {
return _name;
}
QString Card::last4() const {
return _last4;
}
QString Card::dynamicLast4() const {
return _dynamicLast4;
}
CardBrand Card::brand() const {
return _brand;
}
CardFundingType Card::funding() const {
return _funding;
}
QString Card::fingerprint() const {
return _fingerprint;
}
QString Card::country() const {
return _country;
}
QString Card::currency() const {
return _currency;
}
quint32 Card::expMonth() const {
return _expMonth;
}
quint32 Card::expYear() const {
return _expYear;
}
QString Card::addressLine1() const {
return _addressLine1;
}
QString Card::addressLine2() const {
return _addressLine2;
}
QString Card::addressCity() const {
return _addressCity;
}
QString Card::addressState() const {
return _addressState;
}
QString Card::addressZip() const {
return _addressZip;
}
QString Card::addressCountry() const {
return _addressCountry;
}
bool Card::empty() const {
return _cardId.isEmpty();
}
QString CardBrandToString(CardBrand brand) {
switch (brand) {
case CardBrand::Amex: return "American Express";
case CardBrand::DinersClub: return "Diners Club";
case CardBrand::Discover: return "Discover";
case CardBrand::JCB: return "JCB";
case CardBrand::MasterCard: return "MasterCard";
case CardBrand::Unknown: return "Unknown";
case CardBrand::Visa: return "Visa";
}
std::abort();
}
} // namespace Stripe

View File

@@ -0,0 +1,99 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace Stripe {
enum class CardBrand {
Visa,
Amex,
MasterCard,
Discover,
JCB,
DinersClub,
UnionPay,
Unknown,
};
enum class CardFundingType {
Debit,
Credit,
Prepaid,
Other,
};
class Card final {
public:
Card(const Card &other) = default;
Card &operator=(const Card &other) = default;
Card(Card &&other) = default;
Card &operator=(Card &&other) = default;
~Card() = default;
[[nodiscard]] static Card Empty();
[[nodiscard]] static Card DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] QString cardId() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString last4() const;
[[nodiscard]] QString dynamicLast4() const;
[[nodiscard]] CardBrand brand() const;
[[nodiscard]] CardFundingType funding() const;
[[nodiscard]] QString fingerprint() const;
[[nodiscard]] QString country() const;
[[nodiscard]] QString currency() const;
[[nodiscard]] quint32 expMonth() const;
[[nodiscard]] quint32 expYear() const;
[[nodiscard]] QString addressLine1() const;
[[nodiscard]] QString addressLine2() const;
[[nodiscard]] QString addressCity() const;
[[nodiscard]] QString addressState() const;
[[nodiscard]] QString addressZip() const;
[[nodiscard]] QString addressCountry() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Card(
QString id,
QString last4,
CardBrand brand,
quint32 expMonth,
quint32 expYear);
QString _cardId;
QString _name;
QString _last4;
QString _dynamicLast4;
CardBrand _brand = CardBrand::Unknown;
CardFundingType _funding = CardFundingType::Other;
QString _fingerprint;
QString _country;
QString _currency;
quint32 _expMonth = 0;
quint32 _expYear = 0;
QString _addressLine1;
QString _addressLine2;
QString _addressCity;
QString _addressState;
QString _addressZip;
QString _addressCountry;
};
[[nodiscard]] QString CardBrandToString(CardBrand brand);
} // namespace Stripe

View File

@@ -0,0 +1,33 @@
/*
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 "stripe/stripe_card_params.h"
namespace Stripe {
QString CardParams::rootObjectName() {
return "card";
}
std::map<QString, QString> CardParams::formFieldValues() const {
return {
{ "number", number },
{ "cvc", cvc },
{ "name", name },
{ "address_line1", addressLine1 },
{ "address_line2", addressLine2 },
{ "address_city", addressCity },
{ "address_state", addressState },
{ "address_zip", addressZip },
{ "address_country", addressCountry },
{ "exp_month", QString::number(expMonth) },
{ "exp_year", QString::number(expYear) },
{ "currency", currency },
};
}
} // namespace Stripe

View File

@@ -0,0 +1,32 @@
/*
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 "stripe/stripe_form_encodable.h"
namespace Stripe {
struct CardParams {
QString number;
quint32 expMonth = 0;
quint32 expYear = 0;
QString cvc;
QString name;
QString addressLine1;
QString addressLine2;
QString addressCity;
QString addressState;
QString addressZip;
QString addressCountry;
QString currency;
[[nodiscard]] static QString rootObjectName();
[[nodiscard]] std::map<QString, QString> formFieldValues() const;
};
} // namespace Stripe

View File

@@ -0,0 +1,304 @@
/*
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 "stripe/stripe_card_validator.h"
#include "base/qt/qt_string_view.h"
#include <QtCore/QDate>
#include <QtCore/QRegularExpression>
namespace Stripe {
namespace {
constexpr auto kMinCvcLength = 3;
struct BinRange {
QString low;
QString high;
int length = 0;
CardBrand brand = CardBrand::Unknown;
};
[[nodiscard]] const std::vector<BinRange> &AllRanges() {
static auto kResult = std::vector<BinRange>{
// Unknown
{ "", "", 19, CardBrand::Unknown },
// American Express
{ "34", "34", 15, CardBrand::Amex },
{ "37", "37", 15, CardBrand::Amex },
// Diners Club
{ "30", "30", 16, CardBrand::DinersClub },
{ "36", "36", 14, CardBrand::DinersClub },
{ "38", "39", 16, CardBrand::DinersClub },
// Discover
{ "60", "60", 16, CardBrand::Discover },
{ "64", "65", 16, CardBrand::Discover },
// JCB
{ "35", "35", 16, CardBrand::JCB },
// Mastercard
{ "50", "59", 16, CardBrand::MasterCard },
{ "22", "27", 16, CardBrand::MasterCard },
{ "67", "67", 16, CardBrand::MasterCard }, // Maestro
// UnionPay
{ "62", "62", 16, CardBrand::UnionPay },
{ "81", "81", 16, CardBrand::UnionPay },
// Visa
{ "40", "49", 16, CardBrand::Visa },
{ "413600", "413600", 13, CardBrand::Visa },
{ "444509", "444509", 13, CardBrand::Visa },
{ "444509", "444509", 13, CardBrand::Visa },
{ "444550", "444550", 13, CardBrand::Visa },
{ "450603", "450603", 13, CardBrand::Visa },
{ "450617", "450617", 13, CardBrand::Visa },
{ "450628", "450629", 13, CardBrand::Visa },
{ "450636", "450636", 13, CardBrand::Visa },
{ "450640", "450641", 13, CardBrand::Visa },
{ "450662", "450662", 13, CardBrand::Visa },
{ "463100", "463100", 13, CardBrand::Visa },
{ "476142", "476142", 13, CardBrand::Visa },
{ "476143", "476143", 13, CardBrand::Visa },
{ "492901", "492902", 13, CardBrand::Visa },
{ "492920", "492920", 13, CardBrand::Visa },
{ "492923", "492923", 13, CardBrand::Visa },
{ "492928", "492930", 13, CardBrand::Visa },
{ "492937", "492937", 13, CardBrand::Visa },
{ "492939", "492939", 13, CardBrand::Visa },
{ "492960", "492960", 13, CardBrand::Visa },
};
return kResult;
}
[[nodiscard]] bool BinRangeMatchesNumber(
const BinRange &range,
const QString &sanitized) {
const auto minWithLow = std::min(sanitized.size(), range.low.size());
if (base::StringViewMid(sanitized, 0, minWithLow).toInt()
< base::StringViewMid(range.low, 0, minWithLow).toInt()) {
return false;
}
const auto minWithHigh = std::min(sanitized.size(), range.high.size());
if (base::StringViewMid(sanitized, 0, minWithHigh).toInt()
> base::StringViewMid(range.high, 0, minWithHigh).toInt()) {
return false;
}
return true;
}
[[nodiscard]] bool IsNumeric(const QString &value) {
static const auto RegExp = QRegularExpression("^[0-9]*$");
return RegExp.match(value).hasMatch();
}
[[nodiscard]] QString RemoveWhitespaces(QString value) {
static const auto RegExp = QRegularExpression("\\s");
return value.replace(RegExp, QString());
}
[[nodiscard]] std::vector<BinRange> BinRangesForNumber(
const QString &sanitized) {
const auto &all = AllRanges();
auto result = std::vector<BinRange>();
result.reserve(all.size());
for (const auto &range : all) {
if (BinRangeMatchesNumber(range, sanitized)) {
result.push_back(range);
}
}
return result;
}
[[nodiscard]] BinRange MostSpecificBinRangeForNumber(
const QString &sanitized) {
auto possible = BinRangesForNumber(sanitized);
const auto compare = [&](const BinRange &a, const BinRange &b) {
if (sanitized.isEmpty()) {
const auto aUnknown = (a.brand == CardBrand::Unknown);
const auto bUnknown = (b.brand == CardBrand::Unknown);
if (aUnknown && !bUnknown) {
return true;
} else if (!aUnknown && bUnknown) {
return false;
}
}
return a.low.size() < b.low.size();
};
std::sort(begin(possible), end(possible), compare);
return possible.back();
}
[[nodiscard]] int MaxCvcLengthForBranch(CardBrand brand) {
switch (brand) {
case CardBrand::Amex:
case CardBrand::Unknown:
return 4;
default:
return 3;
}
}
[[nodiscard]] std::vector<CardBrand> PossibleBrandsForNumber(
const QString &sanitized) {
const auto ranges = BinRangesForNumber(sanitized);
auto result = std::vector<CardBrand>();
for (const auto &range : ranges) {
const auto brand = range.brand;
if (brand == CardBrand::Unknown
|| (std::find(begin(result), end(result), brand)
!= end(result))) {
continue;
}
result.push_back(brand);
}
return result;
}
[[nodiscard]] CardBrand BrandForNumber(const QString &number) {
const auto sanitized = RemoveWhitespaces(number);
if (!IsNumeric(sanitized)) {
return CardBrand::Unknown;
}
const auto possible = PossibleBrandsForNumber(sanitized);
return (possible.size() == 1) ? possible.front() : CardBrand::Unknown;
}
[[nodiscard]] bool IsValidLuhn(const QString &sanitized) {
auto odd = true;
auto sum = 0;
for (auto i = sanitized.end(); i != sanitized.begin();) {
--i;
auto digit = int(i->unicode() - '0');
odd = !odd;
if (odd) {
digit *= 2;
}
if (digit > 9) {
digit -= 9;
}
sum += digit;
}
return (sum % 10) == 0;
}
} // namespace
CardValidationResult ValidateCard(const QString &number) {
const auto sanitized = RemoveWhitespaces(number);
if (!IsNumeric(sanitized)) {
return { .state = ValidationState::Invalid };
} else if (sanitized.isEmpty()) {
return { .state = ValidationState::Incomplete };
}
const auto range = MostSpecificBinRangeForNumber(sanitized);
const auto brand = range.brand;
static const auto &all = AllRanges();
static const auto compare = [](const BinRange &a, const BinRange &b) {
return a.length < b.length;
};
static const auto kMinLength = std::min_element(
begin(all),
end(all),
compare)->length;
static const auto kMaxLength = std::max_element(
begin(all),
end(all),
compare)->length;
if (sanitized.size() > kMaxLength) {
return { .state = ValidationState::Invalid, .brand = brand };
} else if (sanitized.size() < kMinLength) {
return { .state = ValidationState::Incomplete, .brand = brand };
} else if (!IsValidLuhn(sanitized)) {
return { .state = ValidationState::Invalid, .brand = brand };
} else if (sanitized.size() < kMaxLength) {
return { .state = ValidationState::Valid, .brand = brand };
}
return {
.state = ValidationState::Valid,
.brand = brand,
.finished = true,
};
}
ExpireDateValidationResult ValidateExpireDate(
const QString &date,
const std::optional<QDate> &overrideExpireDateThreshold) {
const auto sanitized = RemoveWhitespaces(date).replace('/', QString());
if (!IsNumeric(sanitized)) {
return { ValidationState::Invalid };
} else if (sanitized.size() < 2) {
return { ValidationState::Incomplete };
}
const auto normalized = (sanitized[0] > '1' ? "0" : "") + sanitized;
const auto month = base::StringViewMid(normalized, 0, 2).toInt();
if (month < 1 || month > 12) {
return { ValidationState::Invalid };
} else if (normalized.size() < 4) {
return { ValidationState::Incomplete };
} else if (normalized.size() > 4) {
return { ValidationState::Invalid };
}
const auto year = 2000 + base::StringViewMid(normalized, 2).toInt();
const auto thresholdDate = overrideExpireDateThreshold.value_or(
QDate::currentDate());
const auto thresholdMonth = thresholdDate.month();
const auto thresholdYear = thresholdDate.year();
if (year < thresholdYear) {
return { ValidationState::Invalid };
} else if (year == thresholdYear && month < thresholdMonth) {
return { ValidationState::Invalid };
}
return { ValidationState::Valid, true };
}
ValidationState ValidateParsedExpireDate(
quint32 month,
quint32 year,
const std::optional<QDate> &overrideExpireDateThreshold) {
if ((year / 100) != 20) {
return ValidationState::Invalid;
}
const auto date = QString("%1%2"
).arg(month, 2, 10, QChar('0')
).arg(year % 100, 2, 10, QChar('0'));
return ValidateExpireDate(date, overrideExpireDateThreshold).state;
}
CvcValidationResult ValidateCvc(
const QString &number,
const QString &cvc) {
if (!IsNumeric(cvc)) {
return { ValidationState::Invalid };
} else if (cvc.size() < kMinCvcLength) {
return { ValidationState::Incomplete };
}
const auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number));
if (cvc.size() > maxLength) {
return { ValidationState::Invalid };
}
return { ValidationState::Valid, (cvc.size() == maxLength) };
}
std::vector<int> CardNumberFormat(const QString &number) {
static const auto kDefault = std::vector{ 4, 4, 4, 4 };
const auto sanitized = RemoveWhitespaces(number);
if (!IsNumeric(sanitized)) {
return kDefault;
}
const auto range = MostSpecificBinRangeForNumber(sanitized);
if (range.brand == CardBrand::DinersClub && range.length == 14) {
return { 4, 6, 4 };
} else if (range.brand == CardBrand::Amex) {
return { 4, 6, 5 };
}
return kDefault;
}
} // namespace Stripe

View File

@@ -0,0 +1,57 @@
/*
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 "stripe/stripe_card.h"
#include <vector>
#include <optional>
class QDate;
namespace Stripe {
enum class ValidationState {
Invalid,
Incomplete,
Valid,
};
struct CardValidationResult {
ValidationState state = ValidationState::Invalid;
CardBrand brand = CardBrand::Unknown;
bool finished = false;
};
[[nodiscard]] CardValidationResult ValidateCard(const QString &number);
struct ExpireDateValidationResult {
ValidationState state = ValidationState::Invalid;
bool finished = false;
};
[[nodiscard]] ExpireDateValidationResult ValidateExpireDate(
const QString &date,
const std::optional<QDate> &overrideExpireDateThreshold);
[[nodiscard]] ValidationState ValidateParsedExpireDate(
quint32 month,
quint32 year,
const std::optional<QDate> &overrideExpireDateThreshold);
struct CvcValidationResult {
ValidationState state = ValidationState::Invalid;
bool finished = false;
};
[[nodiscard]] CvcValidationResult ValidateCvc(
const QString &number,
const QString &cvc);
[[nodiscard]] std::vector<int> CardNumberFormat(const QString &number);
} // namespace Stripe

View File

@@ -0,0 +1,23 @@
/*
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 "stripe/stripe_decode.h"
namespace Stripe {
[[nodiscard]] bool ContainsFields(
const QJsonObject &object,
std::vector<QStringView> keys) {
for (const auto &key : keys) {
if (object.value(key).isUndefined()) {
return false;
}
}
return true;
}
} // namespace Stripe

View File

@@ -0,0 +1,19 @@
/*
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 <QtCore/QJsonObject>
#include <vector>
namespace Stripe {
[[nodiscard]] bool ContainsFields(
const QJsonObject &object,
std::vector<QStringView> keys);
} // namespace Stripe

View File

@@ -0,0 +1,107 @@
/*
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 "stripe/stripe_error.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
Error::Code Error::code() const {
return _code;
}
QString Error::description() const {
return _description;
}
QString Error::message() const {
return _message;
}
QString Error::parameter() const {
return _parameter;
}
Error Error::None() {
return Error(Code::None, {}, {}, {});
}
Error Error::DecodedObjectFromResponse(QJsonObject object) {
const auto entry = object.value("error");
if (!entry.isObject()) {
return Error::None();
}
const auto error = entry.toObject();
const auto string = [&](QStringView key) {
return error.value(key).toString();
};
const auto type = string(u"type");
const auto message = string(u"message");
const auto parameterSnakeCase = string(u"param");
// There should always be a message and type for the error
if (message.isEmpty() || type.isEmpty()) {
return {
Code::API,
"GenericError",
"Could not interpret the error response "
"that was returned from Stripe."
};
}
auto parameterWords = parameterSnakeCase.isEmpty()
? QStringList()
: parameterSnakeCase.split('_', Qt::SkipEmptyParts);
auto first = true;
for (auto &word : parameterWords) {
if (first) {
first = false;
} else {
word = word[0].toUpper() + word.mid(1);
}
}
const auto parameter = parameterWords.join(QString());
if (type == "api_error") {
return { Code::API, "GenericError", message, parameter };
} else if (type == "invalid_request_error") {
return { Code::InvalidRequest, "GenericError", message, parameter };
} else if (type != "card_error") {
return { Code::Unknown, type, message, parameter };
}
const auto code = string(u"code");
const auto cardError = [&](const QString &description) {
return Error{ Code::Card, description, message, parameter };
};
if (code == "incorrect_number") {
return cardError("IncorrectNumber");
} else if (code == "invalid_number") {
return cardError("InvalidNumber");
} else if (code == "invalid_expiry_month") {
return cardError("InvalidExpiryMonth");
} else if (code == "invalid_expiry_year") {
return cardError("InvalidExpiryYear");
} else if (code == "invalid_cvc") {
return cardError("InvalidCVC");
} else if (code == "expired_card") {
return cardError("ExpiredCard");
} else if (code == "incorrect_cvc") {
return cardError("IncorrectCVC");
} else if (code == "card_declined") {
return cardError("CardDeclined");
} else if (code == "processing_error") {
return cardError("ProcessingError");
} else {
return cardError(code);
}
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace Stripe

View File

@@ -0,0 +1,66 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace Stripe {
class Error {
public:
enum class Code {
None = 0, // Non-Stripe errors.
JsonParse = -1,
JsonFormat = -2,
Network = -3,
Unknown = 8,
Connection = 40, // Trouble connecting to Stripe.
InvalidRequest = 50, // Your request had invalid parameters.
API = 60, // General-purpose API error (should be rare).
Card = 70, // Something was wrong with the given card (most common).
Cancellation = 80, // The operation was cancelled.
CheckoutUnknown = 5000, // Checkout failed
CheckoutTooManyAttempts = 5001, // Too many incorrect code attempts
};
Error(
Code code,
const QString &description,
const QString &message,
const QString &parameter = QString())
: _code(code)
, _description(description)
, _message(message)
, _parameter(parameter) {
}
[[nodiscard]] Code code() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString parameter() const;
[[nodiscard]] static Error None();
[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Code _code = Code::None;
QString _description;
QString _message;
QString _parameter;
};
} // namespace Stripe

View File

@@ -0,0 +1,39 @@
/*
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 <QtCore/QString>
#include <map>
namespace Stripe {
class FormEncodable {
public:
[[nodiscard]] virtual QString rootObjectName() = 0;
[[nodiscard]] virtual std::map<QString, QString> formFieldValues() = 0;
};
template <typename T>
struct MakeEncodable final : FormEncodable {
public:
MakeEncodable(const T &value) : _value(value) {
}
QString rootObjectName() override {
return _value.rootObjectName();
}
std::map<QString, QString> formFieldValues() override {
return _value.formFieldValues();
}
private:
const T &_value;
};
} // namespace Stripe

View File

@@ -0,0 +1,41 @@
/*
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 "stripe/stripe_form_encoder.h"
#include <QStringList>
#include <QUrl>
#include <vector>
namespace Stripe {
QByteArray FormEncoder::formEncodedDataForObject(
FormEncodable &&object) {
const auto root = object.rootObjectName();
const auto values = object.formFieldValues();
auto result = QByteArray();
auto keys = std::vector<QString>();
for (const auto &[key, value] : values) {
if (!value.isEmpty()) {
keys.push_back(key);
}
}
std::sort(begin(keys), end(keys));
const auto encode = [](const QString &value) {
return QUrl::toPercentEncoding(value);
};
for (const auto &key : keys) {
const auto fullKey = root.isEmpty() ? key : (root + '[' + key + ']');
if (!result.isEmpty()) {
result += '&';
}
result += encode(fullKey) + '=' + encode(values.at(key));
}
return result;
}
} // namespace Stripe

View File

@@ -0,0 +1,21 @@
/*
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 "stripe/stripe_form_encodable.h"
namespace Stripe {
class FormEncoder {
public:
[[nodiscard]] static QByteArray formEncodedDataForObject(
FormEncodable &&object);
};
} // namespace Stripe

View File

@@ -0,0 +1,29 @@
/*
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 "stripe/stripe_address.h"
#include <QtCore/QString>
namespace Stripe {
struct PaymentConfiguration {
QString publishableKey;
// PaymentMethodType additionalPaymentMethods; // Apply Pay
// TODO incomplete, not used.
//BillingAddressFields requiredBillingAddressFields
// = BillingAddressFields::None;
QString companyName;
// QString appleMerchantIdentifier; // Apple Pay
// bool smsAutofillDisabled = true; // Mobile only
};
} // namespace Stripe

View File

@@ -0,0 +1,13 @@
/*
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 <QtCore/QString>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonDocument>

View File

@@ -0,0 +1,65 @@
/*
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 "stripe/stripe_token.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
QString Token::tokenId() const {
return _tokenId;
}
bool Token::livemode() const {
return _livemode;
}
Card Token::card() const {
return _card;
}
Token Token::Empty() {
return Token(QString(), false, QDateTime());
}
Token Token::DecodedObjectFromAPIResponse(QJsonObject object) {
if (!ContainsFields(object, { u"id", u"livemode", u"created" })) {
return Token::Empty();
}
const auto tokenId = object.value("id").toString();
const auto livemode = object.value("livemode").toBool();
const auto created = QDateTime::fromSecsSinceEpoch(
object.value("created").toDouble());
auto result = Token(tokenId, livemode, created);
const auto card = object.value("card");
if (card.isObject()) {
result._card = Card::DecodedObjectFromAPIResponse(card.toObject());
}
// TODO incomplete, not used.
//const auto bankAccount = object.value("bank_account");
//if (bankAccount.isObject()) {
// result._bankAccount = bankAccount::DecodedObjectFromAPIResponse(
// bankAccount.toObject());
//}
//result._allResponseFields = object;
return result;
}
bool Token::empty() const {
return _tokenId.isEmpty();
}
Token::Token(QString tokenId, bool livemode, QDateTime created)
: _tokenId(std::move(tokenId))
, _livemode(livemode)
, _created(std::move(created)) {
}
} // namespace Stripe

View File

@@ -0,0 +1,49 @@
/*
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 "stripe/stripe_card.h"
#include <QtCore/QDateTime>
class QJsonObject;
namespace Stripe {
class Token {
public:
Token(const Token &other) = default;
Token &operator=(const Token &other) = default;
Token(Token &&other) = default;
Token &operator=(Token &&other) = default;
~Token() = default;
[[nodiscard]] QString tokenId() const;
[[nodiscard]] bool livemode() const;
[[nodiscard]] Card card() const;
[[nodiscard]] static Token Empty();
[[nodiscard]] static Token DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Token(QString tokenId, bool livemode, QDateTime created);
QString _tokenId;
bool _livemode = false;
QDateTime _created;
Card _card = Card::Empty();
};
} // namespace Stripe

View File

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

View File

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

View File

@@ -0,0 +1,72 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class Checkbox;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class Field;
class EditCard final : public RpWidget {
public:
EditCard(
QWidget *parent,
const NativeMethodDetails &native,
CardField field,
not_null<PanelDelegate*> delegate);
void setFocus(CardField field);
void setFocusFast(CardField field);
void showError(CardField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(CardField field) const;
[[nodiscard]] UncheckedCardDetails collect() const;
const not_null<PanelDelegate*> _delegate;
NativeMethodDetails _native;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
std::unique_ptr<Field> _number;
std::unique_ptr<Field> _cvc;
std::unique_ptr<Field> _expire;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _zip;
Checkbox *_save = nullptr;
CardField _focusField = CardField::Number;
};
} // namespace Payments::Ui

View File

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

View File

@@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class InputField;
class MaskedInputField;
class Checkbox;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class Field;
class EditInformation final : public RpWidget {
public:
EditInformation(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
InformationField field,
not_null<PanelDelegate*> delegate);
~EditInformation();
void setFocus(InformationField field);
void setFocusFast(InformationField field);
void showError(InformationField field);
private:
void resizeEvent(QResizeEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void updateControlsGeometry();
[[nodiscard]] Field *lookupField(InformationField field) const;
[[nodiscard]] RequestedInformation collect() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
std::unique_ptr<Field> _street1;
std::unique_ptr<Field> _street2;
std::unique_ptr<Field> _city;
std::unique_ptr<Field> _state;
std::unique_ptr<Field> _country;
std::unique_ptr<Field> _postcode;
std::unique_ptr<Field> _name;
std::unique_ptr<Field> _email;
std::unique_ptr<Field> _phone;
Checkbox *_save = nullptr;
InformationField _focusField = InformationField::ShippingStreet;
};
} // namespace Payments::Ui

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "payments/ui/payments_panel_data.h"
#include "base/object_ptr.h"
#include "styles/style_widgets.h"
namespace Ui {
class ScrollArea;
class FadeShadow;
class RoundButton;
class VerticalLayout;
} // namespace Ui
namespace Payments::Ui {
using namespace ::Ui;
class PanelDelegate;
class FormSummary final : public RpWidget {
public:
FormSummary(
QWidget *parent,
const Invoice &invoice,
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options,
not_null<PanelDelegate*> delegate,
int scrollTop);
void updateThumbnail(const QImage &thumbnail);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
bool showCriticalError(const TextWithEntities &text);
[[nodiscard]] int contentHeight() const;
private:
void resizeEvent(QResizeEvent *e) override;
void setupControls();
void setupContent(not_null<VerticalLayout*> layout);
void setupCover(not_null<VerticalLayout*> layout);
void setupPrices(not_null<VerticalLayout*> layout);
void setupSuggestedTips(not_null<VerticalLayout*> layout);
void setupSections(not_null<VerticalLayout*> layout);
void updateControlsGeometry();
[[nodiscard]] QString formatAmount(
int64 amount,
bool forceStripDotZero = false) const;
[[nodiscard]] int64 computeTotalAmount() const;
const not_null<PanelDelegate*> _delegate;
Invoice _invoice;
PaymentMethodDetails _method;
ShippingOptions _options;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
not_null<VerticalLayout*> _layout;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;
object_ptr<RoundButton> _cancel;
rpl::event_stream<QImage> _thumbnails;
style::complex_color _tipLightBg;
style::complex_color _tipLightRipple;
style::complex_color _tipChosenBg;
style::complex_color _tipChosenRipple;
style::RoundButton _tipButton;
style::RoundButton _tipChosen;
int _initialScrollTop = 0;
};
} // namespace Payments::Ui

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
class QJsonDocument;
class QString;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Webview {
struct ThemeParams;
struct StorageId;
} // namespace Webview
namespace Payments::Ui {
using namespace ::Ui;
struct RequestedInformation;
struct UncheckedCardDetails;
class PanelDelegate {
public:
virtual void panelRequestClose() = 0;
virtual void panelCloseSure() = 0;
virtual void panelSubmit() = 0;
virtual void panelTrustAndSubmit() = 0;
virtual void panelAcceptTermsAndSubmit() = 0;
virtual void panelWebviewMessage(
const QJsonDocument &message,
bool saveInformation) = 0;
virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;
virtual void panelSetPassword() = 0;
virtual void panelOpenUrl(const QString &url) = 0;
virtual void panelCancelEdit() = 0;
virtual void panelEditPaymentMethod() = 0;
virtual void panelEditShippingInformation() = 0;
virtual void panelEditName() = 0;
virtual void panelEditEmail() = 0;
virtual void panelEditPhone() = 0;
virtual void panelChooseShippingOption() = 0;
virtual void panelChangeShippingOption(const QString &id) = 0;
virtual void panelChooseTips() = 0;
virtual void panelChangeTips(int64 value) = 0;
virtual void panelValidateInformation(RequestedInformation data) = 0;
virtual void panelValidateCard(
Ui::UncheckedCardDetails data,
bool saveInformation) = 0;
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
virtual QVariant panelClickHandlerContext() = 0;
virtual Webview::StorageId panelWebviewStorageId() = 0;
virtual Webview::ThemeParams panelWebviewThemeParams() = 0;
virtual std::optional<QDate> panelOverrideExpireDateThreshold() = 0;
};
} // namespace Payments::Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
namespace style {
struct RoundCheckbox;
struct MediaSlider;
} // namespace style
namespace Main {
class Session;
} // namespace Main
namespace Ui::Premium {
class BubbleWidget;
} // namespace Ui::Premium
namespace Ui {
class AbstractButton;
class BoxContent;
class GenericBox;
class DynamicImage;
class VerticalLayout;
struct PaidReactionTop {
QString name;
std::shared_ptr<DynamicImage> photo;
uint64 barePeerId = 0;
int count = 0;
Fn<void()> click;
bool my = false;
};
struct PaidReactionBoxArgs {
int min = 0;
int explicitlyAllowed = 0;
int chosen = 0;
int max = 0;
std::vector<PaidReactionTop> top;
not_null<Main::Session*> session;
QString name;
Fn<rpl::producer<TextWithEntities>(rpl::producer<int> amount)> submit;
std::vector<Calls::Group::Ui::StarsColoring> colorings;
rpl::producer<CreditsAmount> balanceValue;
Fn<void(int, uint64)> send;
bool videoStreamChoosing = false;
bool videoStreamSending = false;
bool videoStreamAdmin = false;
bool dark = false;
};
void PaidReactionsBox(
not_null<GenericBox*> box,
PaidReactionBoxArgs &&args);
[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(
PaidReactionBoxArgs &&args);
[[nodiscard]] int MaxTopPaidDonorsShown();
[[nodiscard]] QImage GenerateSmallBadgeImage(
QString text,
const style::icon &icon,
QColor bg,
QColor fg,
const style::RoundCheckbox *borderSt = nullptr);
struct StarSelectDiscreter {
Fn<int(float64)> ratioToValue;
Fn<float64(int)> valueToRatio;
};
[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max);
void PaidReactionSlider(
not_null<VerticalLayout*> container,
const style::MediaSlider &st,
int min,
int explicitlyAllowed,
rpl::producer<int> current,
int max,
Fn<void(int)> changed,
Fn<QColor(int)> activeFgOverride = nullptr);
void AddStarSelectBalance(
not_null<GenericBox*> box,
not_null<Main::Session*> session,
rpl::producer<CreditsAmount> balanceValue,
bool dark = false);
not_null<Premium::BubbleWidget*> AddStarSelectBubble(
not_null<VerticalLayout*> container,
rpl::producer<> showFinishes,
rpl::producer<int> value,
int max,
Fn<QColor(int)> activeFgOverride = nullptr);
struct StarSelectInfoBlock {
rpl::producer<TextWithEntities> title;
rpl::producer<QString> subtext;
Fn<void()> click;
};
[[nodiscard]] object_ptr<RpWidget> MakeStarSelectInfoBlocks(
not_null<RpWidget*> parent,
std::vector<StarSelectInfoBlock> blocks,
Text::MarkedContext context,
bool dark = false);
} // namespace Ui