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
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:
978
Telegram/SourceFiles/payments/payments_checkout_process.cpp
Normal file
978
Telegram/SourceFiles/payments/payments_checkout_process.cpp
Normal 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
|
||||
206
Telegram/SourceFiles/payments/payments_checkout_process.h
Normal file
206
Telegram/SourceFiles/payments/payments_checkout_process.h
Normal 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
|
||||
1351
Telegram/SourceFiles/payments/payments_form.cpp
Normal file
1351
Telegram/SourceFiles/payments/payments_form.cpp
Normal file
File diff suppressed because it is too large
Load Diff
423
Telegram/SourceFiles/payments/payments_form.h
Normal file
423
Telegram/SourceFiles/payments/payments_form.h
Normal 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
|
||||
184
Telegram/SourceFiles/payments/payments_non_panel_process.cpp
Normal file
184
Telegram/SourceFiles/payments/payments_non_panel_process.cpp
Normal 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
|
||||
48
Telegram/SourceFiles/payments/payments_non_panel_process.h
Normal file
48
Telegram/SourceFiles/payments/payments_non_panel_process.h
Normal 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
|
||||
309
Telegram/SourceFiles/payments/payments_reaction_process.cpp
Normal file
309
Telegram/SourceFiles/payments/payments_reaction_process.cpp
Normal 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
|
||||
54
Telegram/SourceFiles/payments/payments_reaction_process.h
Normal file
54
Telegram/SourceFiles/payments/payments_reaction_process.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
51
Telegram/SourceFiles/payments/smartglocal/smartglocal_card.h
Normal file
51
Telegram/SourceFiles/payments/smartglocal/smartglocal_card.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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 ¶meter = 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
18
Telegram/SourceFiles/payments/stripe/stripe_address.h
Normal file
18
Telegram/SourceFiles/payments/stripe/stripe_address.h
Normal 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
|
||||
167
Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp
Normal file
167
Telegram/SourceFiles/payments/stripe/stripe_api_client.cpp
Normal 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
|
||||
45
Telegram/SourceFiles/payments/stripe/stripe_api_client.h
Normal file
45
Telegram/SourceFiles/payments/stripe/stripe_api_client.h
Normal 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
|
||||
19
Telegram/SourceFiles/payments/stripe/stripe_callbacks.h
Normal file
19
Telegram/SourceFiles/payments/stripe/stripe_callbacks.h
Normal 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
|
||||
188
Telegram/SourceFiles/payments/stripe/stripe_card.cpp
Normal file
188
Telegram/SourceFiles/payments/stripe/stripe_card.cpp
Normal 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
|
||||
99
Telegram/SourceFiles/payments/stripe/stripe_card.h
Normal file
99
Telegram/SourceFiles/payments/stripe/stripe_card.h
Normal 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
|
||||
33
Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp
Normal file
33
Telegram/SourceFiles/payments/stripe/stripe_card_params.cpp
Normal 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
|
||||
32
Telegram/SourceFiles/payments/stripe/stripe_card_params.h
Normal file
32
Telegram/SourceFiles/payments/stripe/stripe_card_params.h
Normal 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
|
||||
304
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal file
304
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/payments/stripe/stripe_card_validator.h
Normal file
57
Telegram/SourceFiles/payments/stripe/stripe_card_validator.h
Normal 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
|
||||
23
Telegram/SourceFiles/payments/stripe/stripe_decode.cpp
Normal file
23
Telegram/SourceFiles/payments/stripe/stripe_decode.cpp
Normal 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
|
||||
19
Telegram/SourceFiles/payments/stripe/stripe_decode.h
Normal file
19
Telegram/SourceFiles/payments/stripe/stripe_decode.h
Normal 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
|
||||
107
Telegram/SourceFiles/payments/stripe/stripe_error.cpp
Normal file
107
Telegram/SourceFiles/payments/stripe/stripe_error.cpp
Normal 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
|
||||
66
Telegram/SourceFiles/payments/stripe/stripe_error.h
Normal file
66
Telegram/SourceFiles/payments/stripe/stripe_error.h
Normal 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 ¶meter = 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
|
||||
39
Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h
Normal file
39
Telegram/SourceFiles/payments/stripe/stripe_form_encodable.h
Normal 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
|
||||
41
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp
Normal file
41
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.cpp
Normal 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
|
||||
21
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h
Normal file
21
Telegram/SourceFiles/payments/stripe/stripe_form_encoder.h
Normal 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
|
||||
@@ -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
|
||||
13
Telegram/SourceFiles/payments/stripe/stripe_pch.h
Normal file
13
Telegram/SourceFiles/payments/stripe/stripe_pch.h
Normal 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>
|
||||
65
Telegram/SourceFiles/payments/stripe/stripe_token.cpp
Normal file
65
Telegram/SourceFiles/payments/stripe/stripe_token.cpp
Normal 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
|
||||
49
Telegram/SourceFiles/payments/stripe/stripe_token.h
Normal file
49
Telegram/SourceFiles/payments/stripe/stripe_token.h
Normal 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
|
||||
156
Telegram/SourceFiles/payments/ui/payments.style
Normal file
156
Telegram/SourceFiles/payments/ui/payments.style
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
using "ui/basic.style";
|
||||
|
||||
using "info/info.style";
|
||||
|
||||
paymentsPanelSize: size(392px, 600px);
|
||||
|
||||
paymentsPanelButton: RoundButton(defaultBoxButton) {
|
||||
width: -36px;
|
||||
height: 36px;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: boxButtonFont;
|
||||
}
|
||||
}
|
||||
paymentsPanelSubmit: RoundButton(defaultActiveButton) {
|
||||
width: -36px;
|
||||
height: 36px;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: boxButtonFont;
|
||||
}
|
||||
}
|
||||
paymentsPanelPadding: margins(8px, 12px, 15px, 12px);
|
||||
|
||||
paymentsCoverPadding: margins(26px, 0px, 26px, 13px);
|
||||
paymentsDescription: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 160px;
|
||||
textFg: windowFg;
|
||||
}
|
||||
paymentsTitle: FlatLabel(paymentsDescription) {
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
paymentsSeller: FlatLabel(paymentsDescription) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
paymentsWebviewBottom: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
paymentsPriceLabel: paymentsDescription;
|
||||
paymentsPriceAmount: defaultFlatLabel;
|
||||
paymentsFullPriceLabel: paymentsTitle;
|
||||
paymentsFullPriceAmount: FlatLabel(defaultFlatLabel) {
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
|
||||
paymentsTitleTop: 0px;
|
||||
paymentsDescriptionTop: 3px;
|
||||
paymentsSellerTop: 4px;
|
||||
|
||||
paymentsThumbnailSize: size(80px, 80px);
|
||||
paymentsThumbnailSkip: 18px;
|
||||
|
||||
paymentsPricesTopSkip: 12px;
|
||||
paymentsPricesBottomSkip: 13px;
|
||||
paymentsPricePadding: margins(28px, 6px, 28px, 5px);
|
||||
|
||||
paymentsTipSkip: 8px;
|
||||
paymentsTipButton: RoundButton(defaultLightButton) {
|
||||
textFg: paymentsTipActive;
|
||||
textFgOver: paymentsTipActive;
|
||||
textBgOver: transparent;
|
||||
|
||||
width: -16px;
|
||||
height: 28px;
|
||||
textTop: 5px;
|
||||
}
|
||||
paymentsTipChosen: RoundButton(paymentsTipButton) {
|
||||
textFg: windowFgActive;
|
||||
textFgOver: windowFgActive;
|
||||
textBgOver: transparent;
|
||||
}
|
||||
paymentsTipButtonsPadding: margins(26px, 6px, 26px, 6px);
|
||||
|
||||
paymentsSectionsTopSkip: 11px;
|
||||
paymentsSectionButton: SettingsButton(infoProfileButton) {
|
||||
padding: margins(68px, 11px, 14px, 9px);
|
||||
}
|
||||
|
||||
paymentsIconPaymentMethod: icon {{ "payments/payment_card", windowBoldFg }};
|
||||
paymentsIconShippingAddress: icon {{ "payments/payment_address", windowBoldFg }};
|
||||
paymentsIconName: icon {{ "payments/payment_name", windowBoldFg }};
|
||||
paymentsIconEmail: icon {{ "payments/payment_email", windowBoldFg }};
|
||||
paymentsIconPhone: icon {{ "payments/payment_phone", windowBoldFg }};
|
||||
paymentsIconShippingMethod: icon {{ "payments/payment_shipping", windowBoldFg }};
|
||||
|
||||
paymentsField: defaultInputField;
|
||||
paymentsMoneyField: InputField(paymentsField) {
|
||||
textMargins: margins(0px, 4px, 0px, 4px);
|
||||
heightMin: 30px;
|
||||
}
|
||||
paymentsFieldAdditional: FlatLabel(defaultFlatLabel) {
|
||||
style: boxTextStyle;
|
||||
}
|
||||
|
||||
paymentsFieldPadding: margins(28px, 0px, 28px, 2px);
|
||||
paymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px);
|
||||
paymentsExpireCvcSkip: 34px;
|
||||
|
||||
paymentsBillingInformationTitle: FlatLabel(defaultFlatLabel) {
|
||||
style: semiboldTextStyle;
|
||||
textFg: windowActiveTextFg;
|
||||
minWidth: 240px;
|
||||
}
|
||||
paymentsBillingInformationTitlePadding: margins(28px, 26px, 28px, 1px);
|
||||
|
||||
paymentsShippingMargin: margins(27px, 11px, 27px, 20px);
|
||||
paymentsShippingLabel: FlatLabel(defaultFlatLabel) {
|
||||
style: boxTextStyle;
|
||||
}
|
||||
paymentsShippingPrice: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
paymentsShippingLabelPosition: point(43px, 8px);
|
||||
paymentsShippingPricePosition: point(43px, 29px);
|
||||
|
||||
paymentTipsErrorLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 275px;
|
||||
textFg: boxTextFgError;
|
||||
}
|
||||
paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px);
|
||||
|
||||
paymentsToProviderLabel: paymentsShippingPrice;
|
||||
paymentsToProviderPadding: margins(28px, 6px, 28px, 6px);
|
||||
|
||||
paymentsCriticalError: FlatLabel(boxLabel) {
|
||||
minWidth: 340px;
|
||||
align: align(top);
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
paymentsCriticalErrorPadding: margins(10px, 40px, 10px, 0px);
|
||||
|
||||
paymentsLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
|
||||
size: size(24px, 24px);
|
||||
color: windowSubTextFg;
|
||||
thickness: 4px;
|
||||
}
|
||||
|
||||
botWebViewPanelSize: size(384px, 694px);
|
||||
botWebViewBottomPadding: margins(12px, 12px, 12px, 12px);
|
||||
botWebViewBottomSkip: point(12px, 8px);
|
||||
botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
|
||||
height: 40px;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: boxButtonFont;
|
||||
}
|
||||
textTop: 11px;
|
||||
}
|
||||
botWebViewRadialStroke: 3px;
|
||||
botWebViewMenu: PopupMenu(popupMenuWithIcons) {
|
||||
maxHeight: 360px;
|
||||
}
|
||||
437
Telegram/SourceFiles/payments/ui/payments_edit_card.cpp
Normal file
437
Telegram/SourceFiles/payments/ui/payments_edit_card.cpp
Normal file
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "payments/ui/payments_edit_card.h"
|
||||
|
||||
#include "payments/ui/payments_panel_delegate.h"
|
||||
#include "payments/ui/payments_field.h"
|
||||
#include "stripe/stripe_card_validator.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_payments.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
struct SimpleFieldState {
|
||||
QString value;
|
||||
int position = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint32 ExtractYear(const QString &value) {
|
||||
return value.split('/').value(1).toInt() + 2000;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32 ExtractMonth(const QString &value) {
|
||||
return value.split('/').value(0).toInt();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString RemoveNonNumbers(QString value) {
|
||||
static const auto RegExp = QRegularExpression("[^0-9]");
|
||||
return value.replace(RegExp, QString());
|
||||
}
|
||||
|
||||
[[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) {
|
||||
return {
|
||||
.value = RemoveNonNumbers(state.value),
|
||||
.position = int(RemoveNonNumbers(
|
||||
state.value.mid(0, state.position)).size()),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] SimpleFieldState PostprocessCardValidateResult(
|
||||
SimpleFieldState result) {
|
||||
const auto groups = Stripe::CardNumberFormat(result.value);
|
||||
auto position = 0;
|
||||
for (const auto length : groups) {
|
||||
position += length;
|
||||
if (position >= result.value.size()) {
|
||||
break;
|
||||
}
|
||||
result.value.insert(position, QChar(' '));
|
||||
if (result.position >= position) {
|
||||
++result.position;
|
||||
}
|
||||
++position;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] SimpleFieldState PostprocessExpireDateValidateResult(
|
||||
SimpleFieldState result) {
|
||||
if (result.value.isEmpty()) {
|
||||
return result;
|
||||
} else if (result.value[0] == '1'
|
||||
&& (result.value.size() > 1)
|
||||
&& result.value[1] > '2') {
|
||||
result.value = result.value.mid(0, 2);
|
||||
return result;
|
||||
} else if (result.value[0] > '1') {
|
||||
result.value = '0' + result.value;
|
||||
++result.position;
|
||||
}
|
||||
if (result.value.size() > 1) {
|
||||
if (result.value.size() > 4) {
|
||||
result.value = result.value.mid(0, 4);
|
||||
}
|
||||
result.value.insert(2, '/');
|
||||
if (result.position >= 2) {
|
||||
++result.position;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
|
||||
return (request.wasAnchor == request.wasPosition)
|
||||
&& (request.wasPosition == request.nowPosition + 1)
|
||||
&& (request.wasValue.mid(0, request.wasPosition - 1)
|
||||
== request.nowValue.mid(0, request.nowPosition))
|
||||
&& (request.wasValue.mid(request.wasPosition)
|
||||
== request.nowValue.mid(request.nowPosition));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
|
||||
return (request.wasAnchor == request.wasPosition)
|
||||
&& (request.wasPosition == request.nowPosition)
|
||||
&& (request.wasValue.mid(0, request.wasPosition)
|
||||
== request.nowValue.mid(0, request.nowPosition))
|
||||
&& (request.wasValue.mid(request.wasPosition + 1)
|
||||
== request.nowValue.mid(request.nowPosition));
|
||||
}
|
||||
|
||||
template <
|
||||
typename ValueValidator,
|
||||
typename ValueValidateResult = decltype(
|
||||
std::declval<ValueValidator>()(QString()))>
|
||||
[[nodiscard]] auto ComplexNumberValidator(
|
||||
ValueValidator valueValidator,
|
||||
Fn<SimpleFieldState(SimpleFieldState)> postprocess) {
|
||||
using namespace Stripe;
|
||||
return [=](FieldValidateRequest request) {
|
||||
const auto realNowState = [&] {
|
||||
const auto backspaced = IsBackspace(request);
|
||||
const auto deleted = IsDelete(request);
|
||||
if (!backspaced && !deleted) {
|
||||
return NumbersOnlyState({
|
||||
.value = request.nowValue,
|
||||
.position = request.nowPosition,
|
||||
});
|
||||
}
|
||||
const auto realWasState = NumbersOnlyState({
|
||||
.value = request.wasValue,
|
||||
.position = request.wasPosition,
|
||||
});
|
||||
const auto changedValue = deleted
|
||||
? (realWasState.value.mid(0, realWasState.position)
|
||||
+ realWasState.value.mid(realWasState.position + 1))
|
||||
: (realWasState.position > 1)
|
||||
? (realWasState.value.mid(0, realWasState.position - 1)
|
||||
+ realWasState.value.mid(realWasState.position))
|
||||
: realWasState.value.mid(realWasState.position);
|
||||
return SimpleFieldState{
|
||||
.value = changedValue,
|
||||
.position = (deleted
|
||||
? realWasState.position
|
||||
: std::max(realWasState.position - 1, 0))
|
||||
};
|
||||
}();
|
||||
const auto result = valueValidator(realNowState.value);
|
||||
const auto postprocessed = postprocess(realNowState);
|
||||
return FieldValidateResult{
|
||||
.value = postprocessed.value,
|
||||
.position = postprocessed.position,
|
||||
.invalid = (result.state == ValidationState::Invalid),
|
||||
.finished = result.finished,
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
[[nodiscard]] auto CardNumberValidator() {
|
||||
return ComplexNumberValidator(
|
||||
Stripe::ValidateCard,
|
||||
PostprocessCardValidateResult);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ExpireDateValidator(
|
||||
const std::optional<QDate> &overrideExpireDateThreshold) {
|
||||
return ComplexNumberValidator([=](const QString &date) {
|
||||
return Stripe::ValidateExpireDate(date, overrideExpireDateThreshold);
|
||||
}, PostprocessExpireDateValidateResult);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto CvcValidator(Fn<QString()> number) {
|
||||
using namespace Stripe;
|
||||
return [=](FieldValidateRequest request) {
|
||||
const auto realNowState = NumbersOnlyState({
|
||||
.value = request.nowValue,
|
||||
.position = request.nowPosition,
|
||||
});
|
||||
const auto result = ValidateCvc(number(), realNowState.value);
|
||||
|
||||
return FieldValidateResult{
|
||||
.value = realNowState.value,
|
||||
.position = realNowState.position,
|
||||
.invalid = (result.state == ValidationState::Invalid),
|
||||
.finished = result.finished,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] auto CardHolderNameValidator() {
|
||||
return [=](FieldValidateRequest request) {
|
||||
return FieldValidateResult{
|
||||
.value = request.nowValue.toUpper(),
|
||||
.position = request.nowPosition,
|
||||
.invalid = request.nowValue.isEmpty(),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditCard::EditCard(
|
||||
QWidget *parent,
|
||||
const NativeMethodDetails &native,
|
||||
CardField field,
|
||||
not_null<PanelDelegate*> delegate)
|
||||
: _delegate(delegate)
|
||||
, _native(native)
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _topShadow(this)
|
||||
, _bottomShadow(this)
|
||||
, _submit(
|
||||
this,
|
||||
tr::lng_about_done(),
|
||||
st::paymentsPanelButton)
|
||||
, _cancel(
|
||||
this,
|
||||
tr::lng_cancel(),
|
||||
st::paymentsPanelButton) {
|
||||
setupControls();
|
||||
}
|
||||
|
||||
void EditCard::setFocus(CardField field) {
|
||||
_focusField = field;
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void EditCard::setFocusFast(CardField field) {
|
||||
_focusField = field;
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->setFocusFast();
|
||||
}
|
||||
}
|
||||
|
||||
void EditCard::showError(CardField field) {
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->showError();
|
||||
}
|
||||
}
|
||||
|
||||
void EditCard::setupControls() {
|
||||
const auto inner = setupContent();
|
||||
|
||||
_submit->addClickHandler([=] {
|
||||
_delegate->panelValidateCard(collect(), (_save && _save->checked()));
|
||||
});
|
||||
_cancel->addClickHandler([=] {
|
||||
_delegate->panelCancelEdit();
|
||||
});
|
||||
|
||||
using namespace rpl::mappers;
|
||||
|
||||
_topShadow->toggleOn(
|
||||
_scroll->scrollTopValue() | rpl::map(_1 > 0));
|
||||
_bottomShadow->toggleOn(rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue(),
|
||||
inner->heightValue(),
|
||||
_1 + _2 < _3));
|
||||
}
|
||||
|
||||
not_null<RpWidget*> EditCard::setupContent() {
|
||||
const auto inner = _scroll->setOwnedWidget(
|
||||
object_ptr<VerticalLayout>(this));
|
||||
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
inner->resizeToWidth(width);
|
||||
}, inner->lifetime());
|
||||
|
||||
const auto showBox = [=](object_ptr<BoxContent> box) {
|
||||
_delegate->panelShowBox(std::move(box));
|
||||
};
|
||||
auto last = (Field*)nullptr;
|
||||
const auto make = [&](QWidget *parent, FieldConfig &&config) {
|
||||
auto result = std::make_unique<Field>(parent, std::move(config));
|
||||
if (last) {
|
||||
last->setNextField(result.get());
|
||||
result->setPreviousField(last);
|
||||
}
|
||||
last = result.get();
|
||||
return result;
|
||||
};
|
||||
const auto add = [&](FieldConfig &&config) {
|
||||
auto result = make(inner, std::move(config));
|
||||
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
|
||||
return result;
|
||||
};
|
||||
_number = add({
|
||||
.type = FieldType::CardNumber,
|
||||
.placeholder = tr::lng_payments_card_number(),
|
||||
.validator = CardNumberValidator(),
|
||||
});
|
||||
auto container = inner->add(
|
||||
object_ptr<FixedHeightWidget>(
|
||||
inner,
|
||||
_number->widget()->height()),
|
||||
st::paymentsFieldPadding);
|
||||
_expire = make(container, {
|
||||
.type = FieldType::CardExpireDate,
|
||||
.placeholder = tr::lng_payments_card_expire_date(),
|
||||
.validator = ExpireDateValidator(
|
||||
_delegate->panelOverrideExpireDateThreshold()),
|
||||
});
|
||||
_cvc = make(container, {
|
||||
.type = FieldType::CardCVC,
|
||||
.placeholder = tr::lng_payments_card_cvc(),
|
||||
.validator = CvcValidator([=] { return _number->value(); }),
|
||||
});
|
||||
container->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto left = (width - st::paymentsExpireCvcSkip) / 2;
|
||||
const auto right = width - st::paymentsExpireCvcSkip - left;
|
||||
_expire->widget()->resizeToWidth(left);
|
||||
_cvc->widget()->resizeToWidth(right);
|
||||
_expire->widget()->moveToLeft(0, 0, width);
|
||||
_cvc->widget()->moveToRight(0, 0, width);
|
||||
}, container->lifetime());
|
||||
|
||||
if (_native.needCardholderName) {
|
||||
_name = add({
|
||||
.type = FieldType::Text,
|
||||
.placeholder = tr::lng_payments_card_holder(),
|
||||
.validator = CardHolderNameValidator(),
|
||||
});
|
||||
}
|
||||
|
||||
if (_native.needCountry || _native.needZip) {
|
||||
inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_payments_billing_address(),
|
||||
st::paymentsBillingInformationTitle),
|
||||
st::paymentsBillingInformationTitlePadding);
|
||||
}
|
||||
if (_native.needCountry) {
|
||||
_country = add({
|
||||
.type = FieldType::Country,
|
||||
.placeholder = tr::lng_payments_billing_country(),
|
||||
.validator = RequiredFinishedValidator(),
|
||||
.showBox = showBox,
|
||||
.defaultCountry = _native.defaultCountry,
|
||||
});
|
||||
}
|
||||
if (_native.needZip) {
|
||||
_zip = add({
|
||||
.type = FieldType::Text,
|
||||
.placeholder = tr::lng_payments_billing_zip_code(),
|
||||
.validator = RequiredValidator(),
|
||||
});
|
||||
if (_country) {
|
||||
_country->finished(
|
||||
) | rpl::on_next([=] {
|
||||
_zip->setFocus();
|
||||
}, lifetime());
|
||||
}
|
||||
}
|
||||
if (_native.canSaveInformation) {
|
||||
_save = inner->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
inner,
|
||||
tr::lng_payments_save_information(tr::now),
|
||||
false),
|
||||
st::paymentsSaveCheckboxPadding);
|
||||
}
|
||||
|
||||
last->submitted(
|
||||
) | rpl::on_next([=] {
|
||||
_delegate->panelValidateCard(collect(), _save && _save->checked());
|
||||
}, lifetime());
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
void EditCard::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void EditCard::focusInEvent(QFocusEvent *e) {
|
||||
if (const auto control = lookupField(_focusField)) {
|
||||
control->setFocusFast();
|
||||
}
|
||||
}
|
||||
|
||||
void EditCard::updateControlsGeometry() {
|
||||
const auto &padding = st::paymentsPanelPadding;
|
||||
const auto buttonsHeight = padding.top()
|
||||
+ _cancel->height()
|
||||
+ padding.bottom();
|
||||
const auto buttonsTop = height() - buttonsHeight;
|
||||
_scroll->setGeometry(0, 0, width(), buttonsTop);
|
||||
_topShadow->resizeToWidth(width());
|
||||
_topShadow->moveToLeft(0, 0);
|
||||
_bottomShadow->resizeToWidth(width());
|
||||
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
|
||||
auto right = padding.right();
|
||||
_submit->moveToRight(right, buttonsTop + padding.top());
|
||||
right += _submit->width() + padding.left();
|
||||
_cancel->moveToRight(right, buttonsTop + padding.top());
|
||||
|
||||
_scroll->updateBars();
|
||||
}
|
||||
|
||||
auto EditCard::lookupField(CardField field) const -> Field* {
|
||||
switch (field) {
|
||||
case CardField::Number: return _number.get();
|
||||
case CardField::Cvc: return _cvc.get();
|
||||
case CardField::ExpireDate: return _expire.get();
|
||||
case CardField::Name: return _name.get();
|
||||
case CardField::AddressCountry: return _country.get();
|
||||
case CardField::AddressZip: return _zip.get();
|
||||
}
|
||||
Unexpected("Unknown field in EditCard::controlForField.");
|
||||
}
|
||||
|
||||
UncheckedCardDetails EditCard::collect() const {
|
||||
return {
|
||||
.number = _number ? _number->value() : QString(),
|
||||
.cvc = _cvc ? _cvc->value() : QString(),
|
||||
.expireYear = _expire ? ExtractYear(_expire->value()) : 0,
|
||||
.expireMonth = _expire ? ExtractMonth(_expire->value()) : 0,
|
||||
.cardholderName = _name ? _name->value() : QString(),
|
||||
.addressCountry = _country ? _country->value() : QString(),
|
||||
.addressZip = _zip ? _zip->value() : QString(),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
72
Telegram/SourceFiles/payments/ui/payments_edit_card.h
Normal file
72
Telegram/SourceFiles/payments/ui/payments_edit_card.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "payments/ui/payments_panel_data.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class FadeShadow;
|
||||
class RoundButton;
|
||||
class Checkbox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class PanelDelegate;
|
||||
class Field;
|
||||
|
||||
class EditCard final : public RpWidget {
|
||||
public:
|
||||
EditCard(
|
||||
QWidget *parent,
|
||||
const NativeMethodDetails &native,
|
||||
CardField field,
|
||||
not_null<PanelDelegate*> delegate);
|
||||
|
||||
void setFocus(CardField field);
|
||||
void setFocusFast(CardField field);
|
||||
void showError(CardField field);
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
|
||||
void setupControls();
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
|
||||
void updateControlsGeometry();
|
||||
[[nodiscard]] Field *lookupField(CardField field) const;
|
||||
|
||||
[[nodiscard]] UncheckedCardDetails collect() const;
|
||||
|
||||
const not_null<PanelDelegate*> _delegate;
|
||||
NativeMethodDetails _native;
|
||||
|
||||
object_ptr<ScrollArea> _scroll;
|
||||
object_ptr<FadeShadow> _topShadow;
|
||||
object_ptr<FadeShadow> _bottomShadow;
|
||||
object_ptr<RoundButton> _submit;
|
||||
object_ptr<RoundButton> _cancel;
|
||||
|
||||
std::unique_ptr<Field> _number;
|
||||
std::unique_ptr<Field> _cvc;
|
||||
std::unique_ptr<Field> _expire;
|
||||
std::unique_ptr<Field> _name;
|
||||
std::unique_ptr<Field> _country;
|
||||
std::unique_ptr<Field> _zip;
|
||||
Checkbox *_save = nullptr;
|
||||
|
||||
CardField _focusField = CardField::Number;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
282
Telegram/SourceFiles/payments/ui/payments_edit_information.cpp
Normal file
282
Telegram/SourceFiles/payments/ui/payments_edit_information.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "payments/ui/payments_edit_information.h"
|
||||
|
||||
#include "payments/ui/payments_panel_delegate.h"
|
||||
#include "payments/ui/payments_field.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_payments.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxStreetSize = 64;
|
||||
constexpr auto kMaxPostcodeSize = 10;
|
||||
constexpr auto kMaxNameSize = 64;
|
||||
constexpr auto kMaxEmailSize = 128;
|
||||
constexpr auto kMaxPhoneSize = 16;
|
||||
constexpr auto kMinCitySize = 2;
|
||||
constexpr auto kMaxCitySize = 64;
|
||||
|
||||
} // namespace
|
||||
|
||||
EditInformation::EditInformation(
|
||||
QWidget *parent,
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field,
|
||||
not_null<PanelDelegate*> delegate)
|
||||
: _delegate(delegate)
|
||||
, _invoice(invoice)
|
||||
, _information(current)
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _topShadow(this)
|
||||
, _bottomShadow(this)
|
||||
, _submit(
|
||||
this,
|
||||
tr::lng_settings_save(),
|
||||
st::paymentsPanelButton)
|
||||
, _cancel(
|
||||
this,
|
||||
tr::lng_cancel(),
|
||||
st::paymentsPanelButton) {
|
||||
setupControls();
|
||||
}
|
||||
|
||||
EditInformation::~EditInformation() = default;
|
||||
|
||||
void EditInformation::setFocus(InformationField field) {
|
||||
_focusField = field;
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void EditInformation::setFocusFast(InformationField field) {
|
||||
_focusField = field;
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->setFocusFast();
|
||||
}
|
||||
}
|
||||
|
||||
void EditInformation::showError(InformationField field) {
|
||||
if (const auto control = lookupField(field)) {
|
||||
_scroll->ensureWidgetVisible(control->widget());
|
||||
control->showError();
|
||||
}
|
||||
}
|
||||
|
||||
void EditInformation::setupControls() {
|
||||
const auto inner = setupContent();
|
||||
|
||||
_submit->addClickHandler([=] {
|
||||
_delegate->panelValidateInformation(collect());
|
||||
});
|
||||
_cancel->addClickHandler([=] {
|
||||
_delegate->panelCancelEdit();
|
||||
});
|
||||
|
||||
using namespace rpl::mappers;
|
||||
|
||||
_topShadow->toggleOn(
|
||||
_scroll->scrollTopValue() | rpl::map(_1 > 0));
|
||||
_bottomShadow->toggleOn(rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue(),
|
||||
inner->heightValue(),
|
||||
_1 + _2 < _3));
|
||||
}
|
||||
|
||||
not_null<RpWidget*> EditInformation::setupContent() {
|
||||
const auto inner = _scroll->setOwnedWidget(
|
||||
object_ptr<VerticalLayout>(this));
|
||||
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
inner->resizeToWidth(width);
|
||||
}, inner->lifetime());
|
||||
|
||||
const auto showBox = [=](object_ptr<BoxContent> box) {
|
||||
_delegate->panelShowBox(std::move(box));
|
||||
};
|
||||
auto last = (Field*)nullptr;
|
||||
const auto add = [&](FieldConfig &&config) {
|
||||
auto result = std::make_unique<Field>(inner, std::move(config));
|
||||
inner->add(result->ownedWidget(), st::paymentsFieldPadding);
|
||||
if (last) {
|
||||
last->setNextField(result.get());
|
||||
result->setPreviousField(last);
|
||||
}
|
||||
last = result.get();
|
||||
return result;
|
||||
};
|
||||
if (_invoice.isShippingAddressRequested) {
|
||||
_street1 = add({
|
||||
.placeholder = tr::lng_payments_address_street1(),
|
||||
.value = _information.shippingAddress.address1,
|
||||
.validator = RangeLengthValidator(1, kMaxStreetSize),
|
||||
});
|
||||
_street2 = add({
|
||||
.placeholder = tr::lng_payments_address_street2(),
|
||||
.value = _information.shippingAddress.address2,
|
||||
.validator = MaxLengthValidator(kMaxStreetSize),
|
||||
});
|
||||
_city = add({
|
||||
.placeholder = tr::lng_payments_address_city(),
|
||||
.value = _information.shippingAddress.city,
|
||||
.validator = RangeLengthValidator(kMinCitySize, kMaxCitySize),
|
||||
});
|
||||
_state = add({
|
||||
.placeholder = tr::lng_payments_address_state(),
|
||||
.value = _information.shippingAddress.state,
|
||||
});
|
||||
_country = add({
|
||||
.type = FieldType::Country,
|
||||
.placeholder = tr::lng_payments_address_country(),
|
||||
.value = _information.shippingAddress.countryIso2,
|
||||
.validator = RequiredFinishedValidator(),
|
||||
.showBox = showBox,
|
||||
.defaultCountry = _information.defaultCountry,
|
||||
});
|
||||
_postcode = add({
|
||||
.placeholder = tr::lng_payments_address_postcode(),
|
||||
.value = _information.shippingAddress.postcode,
|
||||
.validator = RangeLengthValidator(1, kMaxPostcodeSize),
|
||||
});
|
||||
}
|
||||
if (_invoice.isNameRequested) {
|
||||
_name = add({
|
||||
.placeholder = tr::lng_payments_info_name(),
|
||||
.value = _information.name,
|
||||
.validator = RangeLengthValidator(1, kMaxNameSize),
|
||||
});
|
||||
}
|
||||
if (_invoice.isEmailRequested) {
|
||||
_email = add({
|
||||
.type = FieldType::Email,
|
||||
.placeholder = tr::lng_payments_info_email(),
|
||||
.value = _information.email,
|
||||
.validator = RangeLengthValidator(1, kMaxEmailSize),
|
||||
});
|
||||
}
|
||||
if (_invoice.isPhoneRequested) {
|
||||
_phone = add({
|
||||
.type = FieldType::Phone,
|
||||
.placeholder = tr::lng_payments_info_phone(),
|
||||
.value = _information.phone,
|
||||
.validator = RangeLengthValidator(1, kMaxPhoneSize),
|
||||
.defaultPhone = _information.defaultPhone,
|
||||
});
|
||||
}
|
||||
const auto emailToProvider = _invoice.isEmailRequested
|
||||
&& _invoice.emailSentToProvider;
|
||||
const auto phoneToProvider = _invoice.isPhoneRequested
|
||||
&& _invoice.phoneSentToProvider;
|
||||
if (emailToProvider || phoneToProvider) {
|
||||
inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
((emailToProvider && phoneToProvider)
|
||||
? tr::lng_payments_to_provider_phone_email
|
||||
: emailToProvider
|
||||
? tr::lng_payments_to_provider_email
|
||||
: tr::lng_payments_to_provider_phone)(
|
||||
lt_provider,
|
||||
rpl::single(_invoice.provider)),
|
||||
st::paymentsToProviderLabel),
|
||||
st::paymentsToProviderPadding);
|
||||
}
|
||||
_save = inner->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
inner,
|
||||
tr::lng_payments_save_information(tr::now),
|
||||
true),
|
||||
st::paymentsSaveCheckboxPadding);
|
||||
|
||||
if (last) {
|
||||
last->submitted(
|
||||
) | rpl::on_next([=] {
|
||||
_delegate->panelValidateInformation(collect());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
void EditInformation::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void EditInformation::focusInEvent(QFocusEvent *e) {
|
||||
if (const auto control = lookupField(_focusField)) {
|
||||
control->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void EditInformation::updateControlsGeometry() {
|
||||
const auto &padding = st::paymentsPanelPadding;
|
||||
const auto buttonsHeight = padding.top()
|
||||
+ _cancel->height()
|
||||
+ padding.bottom();
|
||||
const auto buttonsTop = height() - buttonsHeight;
|
||||
_scroll->setGeometry(0, 0, width(), buttonsTop);
|
||||
_topShadow->resizeToWidth(width());
|
||||
_topShadow->moveToLeft(0, 0);
|
||||
_bottomShadow->resizeToWidth(width());
|
||||
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
|
||||
auto right = padding.right();
|
||||
_submit->moveToRight(right, buttonsTop + padding.top());
|
||||
right += _submit->width() + padding.left();
|
||||
_cancel->moveToRight(right, buttonsTop + padding.top());
|
||||
|
||||
_scroll->updateBars();
|
||||
}
|
||||
|
||||
auto EditInformation::lookupField(InformationField field) const -> Field* {
|
||||
switch (field) {
|
||||
case InformationField::ShippingStreet: return _street1.get();
|
||||
case InformationField::ShippingCity: return _city.get();
|
||||
case InformationField::ShippingState: return _state.get();
|
||||
case InformationField::ShippingCountry: return _country.get();
|
||||
case InformationField::ShippingPostcode: return _postcode.get();
|
||||
case InformationField::Name: return _name.get();
|
||||
case InformationField::Email: return _email.get();
|
||||
case InformationField::Phone: return _phone.get();
|
||||
}
|
||||
Unexpected("Unknown field in EditInformation::lookupField.");
|
||||
}
|
||||
|
||||
RequestedInformation EditInformation::collect() const {
|
||||
return {
|
||||
.defaultPhone = _information.defaultPhone,
|
||||
.defaultCountry = _information.defaultCountry,
|
||||
.save = _save->checked(),
|
||||
.name = _name ? _name->value() : QString(),
|
||||
.phone = _phone ? _phone->value() : QString(),
|
||||
.email = _email ? _email->value() : QString(),
|
||||
.shippingAddress = {
|
||||
.address1 = _street1 ? _street1->value() : QString(),
|
||||
.address2 = _street2 ? _street2->value() : QString(),
|
||||
.city = _city ? _city->value() : QString(),
|
||||
.state = _state ? _state->value() : QString(),
|
||||
.countryIso2 = _country ? _country->value() : QString(),
|
||||
.postcode = _postcode ? _postcode->value() : QString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
80
Telegram/SourceFiles/payments/ui/payments_edit_information.h
Normal file
80
Telegram/SourceFiles/payments/ui/payments_edit_information.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "payments/ui/payments_panel_data.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class FadeShadow;
|
||||
class RoundButton;
|
||||
class InputField;
|
||||
class MaskedInputField;
|
||||
class Checkbox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class PanelDelegate;
|
||||
class Field;
|
||||
|
||||
class EditInformation final : public RpWidget {
|
||||
public:
|
||||
EditInformation(
|
||||
QWidget *parent,
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field,
|
||||
not_null<PanelDelegate*> delegate);
|
||||
~EditInformation();
|
||||
|
||||
void setFocus(InformationField field);
|
||||
void setFocusFast(InformationField field);
|
||||
void showError(InformationField field);
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
|
||||
void setupControls();
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
|
||||
void updateControlsGeometry();
|
||||
[[nodiscard]] Field *lookupField(InformationField field) const;
|
||||
|
||||
[[nodiscard]] RequestedInformation collect() const;
|
||||
|
||||
const not_null<PanelDelegate*> _delegate;
|
||||
Invoice _invoice;
|
||||
RequestedInformation _information;
|
||||
|
||||
object_ptr<ScrollArea> _scroll;
|
||||
object_ptr<FadeShadow> _topShadow;
|
||||
object_ptr<FadeShadow> _bottomShadow;
|
||||
object_ptr<RoundButton> _submit;
|
||||
object_ptr<RoundButton> _cancel;
|
||||
|
||||
std::unique_ptr<Field> _street1;
|
||||
std::unique_ptr<Field> _street2;
|
||||
std::unique_ptr<Field> _city;
|
||||
std::unique_ptr<Field> _state;
|
||||
std::unique_ptr<Field> _country;
|
||||
std::unique_ptr<Field> _postcode;
|
||||
std::unique_ptr<Field> _name;
|
||||
std::unique_ptr<Field> _email;
|
||||
std::unique_ptr<Field> _phone;
|
||||
Checkbox *_save = nullptr;
|
||||
|
||||
InformationField _focusField = InformationField::ShippingStreet;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
719
Telegram/SourceFiles/payments/ui/payments_field.cpp
Normal file
719
Telegram/SourceFiles/payments/ui/payments_field.cpp
Normal file
@@ -0,0 +1,719 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "payments/ui/payments_field.h"
|
||||
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/boxes/country_select_box.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/fields/special_fields.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "styles/style_payments.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
struct SimpleFieldState {
|
||||
QString value;
|
||||
int position = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {
|
||||
return (rule.thousands == '.' || rule.thousands == ',')
|
||||
? ' '
|
||||
: rule.thousands;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString RemoveNonNumbers(QString value) {
|
||||
static const auto RegExp = QRegularExpression("[^0-9]");
|
||||
return value.replace(RegExp, QString());
|
||||
}
|
||||
|
||||
[[nodiscard]] SimpleFieldState CleanMoneyState(
|
||||
const CurrencyRule &rule,
|
||||
SimpleFieldState state) {
|
||||
const auto withDecimal = state.value.replace(
|
||||
QChar('.'),
|
||||
rule.decimal
|
||||
).replace(
|
||||
QChar(','),
|
||||
rule.decimal
|
||||
);
|
||||
const auto digitsLimit = 16 - rule.exponent;
|
||||
const auto beforePosition = state.value.mid(0, state.position);
|
||||
auto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));
|
||||
if (decimalPosition < 0) {
|
||||
state = {
|
||||
.value = RemoveNonNumbers(state.value),
|
||||
.position = int(RemoveNonNumbers(beforePosition).size()),
|
||||
};
|
||||
} else {
|
||||
const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
|
||||
state.value.mid(0, decimalPosition));
|
||||
state = {
|
||||
.value = (onlyNumbersBeforeDecimal
|
||||
+ QChar(rule.decimal)
|
||||
+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
|
||||
.position = int(RemoveNonNumbers(beforePosition).size()
|
||||
+ (state.position > decimalPosition ? 1 : 0)),
|
||||
};
|
||||
decimalPosition = onlyNumbersBeforeDecimal.size();
|
||||
const auto maxLength = decimalPosition + 1 + rule.exponent;
|
||||
if (state.value.size() > maxLength) {
|
||||
state = {
|
||||
.value = state.value.mid(0, maxLength),
|
||||
.position = std::min(state.position, maxLength),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {
|
||||
state = {
|
||||
.value = QChar('0') + state.value,
|
||||
.position = state.position + 1,
|
||||
};
|
||||
if (decimalPosition >= 0) {
|
||||
++decimalPosition;
|
||||
}
|
||||
}
|
||||
auto skip = 0;
|
||||
while (state.value.size() > skip + 1
|
||||
&& state.value[skip] == QChar('0')
|
||||
&& state.value[skip + 1] != QChar(rule.decimal)) {
|
||||
++skip;
|
||||
}
|
||||
state = {
|
||||
.value = state.value.mid(skip),
|
||||
.position = std::max(state.position - skip, 0),
|
||||
};
|
||||
if (decimalPosition >= 0) {
|
||||
Assert(decimalPosition >= skip);
|
||||
decimalPosition -= skip;
|
||||
if (decimalPosition > digitsLimit) {
|
||||
state = {
|
||||
.value = (state.value.mid(0, digitsLimit)
|
||||
+ state.value.mid(decimalPosition)),
|
||||
.position = (state.position > digitsLimit
|
||||
? std::max(
|
||||
state.position - (decimalPosition - digitsLimit),
|
||||
digitsLimit)
|
||||
: state.position),
|
||||
};
|
||||
}
|
||||
} else if (state.value.size() > digitsLimit) {
|
||||
state = {
|
||||
.value = state.value.mid(0, digitsLimit),
|
||||
.position = std::min(state.position, digitsLimit),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] SimpleFieldState PostprocessMoneyResult(
|
||||
const CurrencyRule &rule,
|
||||
SimpleFieldState result) {
|
||||
const auto position = result.value.indexOf(rule.decimal);
|
||||
const auto from = (position >= 0) ? position : result.value.size();
|
||||
for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {
|
||||
result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));
|
||||
if (result.position >= insertAt) {
|
||||
++result.position;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
|
||||
return (request.wasAnchor == request.wasPosition)
|
||||
&& (request.wasPosition == request.nowPosition + 1)
|
||||
&& (base::StringViewMid(request.wasValue, 0, request.wasPosition - 1)
|
||||
== base::StringViewMid(request.nowValue, 0, request.nowPosition))
|
||||
&& (base::StringViewMid(request.wasValue, request.wasPosition)
|
||||
== base::StringViewMid(request.nowValue, request.nowPosition));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
|
||||
return (request.wasAnchor == request.wasPosition)
|
||||
&& (request.wasPosition == request.nowPosition)
|
||||
&& (base::StringViewMid(request.wasValue, 0, request.wasPosition)
|
||||
== base::StringViewMid(request.nowValue, 0, request.nowPosition))
|
||||
&& (base::StringViewMid(request.wasValue, request.wasPosition + 1)
|
||||
== base::StringViewMid(request.nowValue, request.nowPosition));
|
||||
}
|
||||
|
||||
[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {
|
||||
return [=](FieldValidateRequest request) {
|
||||
const auto realNowState = [&] {
|
||||
const auto backspaced = IsBackspace(request);
|
||||
const auto deleted = IsDelete(request);
|
||||
if (!backspaced && !deleted) {
|
||||
return CleanMoneyState(rule, {
|
||||
.value = request.nowValue,
|
||||
.position = request.nowPosition,
|
||||
});
|
||||
}
|
||||
const auto realWasState = CleanMoneyState(rule, {
|
||||
.value = request.wasValue,
|
||||
.position = request.wasPosition,
|
||||
});
|
||||
const auto changedValue = deleted
|
||||
? (realWasState.value.mid(0, realWasState.position)
|
||||
+ realWasState.value.mid(realWasState.position + 1))
|
||||
: (realWasState.position > 1)
|
||||
? (realWasState.value.mid(0, realWasState.position - 1)
|
||||
+ realWasState.value.mid(realWasState.position))
|
||||
: realWasState.value.mid(realWasState.position);
|
||||
return SimpleFieldState{
|
||||
.value = changedValue,
|
||||
.position = (deleted
|
||||
? realWasState.position
|
||||
: std::max(realWasState.position - 1, 0))
|
||||
};
|
||||
}();
|
||||
const auto postprocessed = PostprocessMoneyResult(
|
||||
rule,
|
||||
realNowState);
|
||||
return FieldValidateResult{
|
||||
.value = postprocessed.value,
|
||||
.position = postprocessed.position,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] QString Parse(const FieldConfig &config) {
|
||||
if (config.type == FieldType::Country) {
|
||||
return Countries::Instance().countryNameByISO2(config.value);
|
||||
} else if (config.type == FieldType::Money) {
|
||||
const auto amount = config.value.toLongLong();
|
||||
if (!amount) {
|
||||
return QString();
|
||||
}
|
||||
const auto rule = LookupCurrencyRule(config.currency);
|
||||
const auto value = std::abs(amount) / std::pow(10., rule.exponent);
|
||||
const auto precision = (!rule.stripDotZero
|
||||
|| std::floor(value) != value)
|
||||
? rule.exponent
|
||||
: 0;
|
||||
return FormatWithSeparators(
|
||||
value,
|
||||
precision,
|
||||
rule.decimal,
|
||||
FieldThousandsSeparator(rule));
|
||||
}
|
||||
return config.value;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString Format(
|
||||
const FieldConfig &config,
|
||||
const QString &parsed,
|
||||
const QString &countryIso2) {
|
||||
if (config.type == FieldType::Country) {
|
||||
return countryIso2;
|
||||
} else if (config.type == FieldType::Money) {
|
||||
static const auto RegExp = QRegularExpression("[^0-9\\.]");
|
||||
const auto rule = LookupCurrencyRule(config.currency);
|
||||
const auto real = QString(parsed).replace(
|
||||
QChar(rule.decimal),
|
||||
QChar('.')
|
||||
).replace(
|
||||
QChar(','),
|
||||
QChar('.')
|
||||
).replace(
|
||||
RegExp,
|
||||
QString()
|
||||
).toDouble();
|
||||
return QString::number(
|
||||
int64(base::SafeRound(real * std::pow(10., rule.exponent))));
|
||||
} else if (config.type == FieldType::CardNumber
|
||||
|| config.type == FieldType::CardCVC) {
|
||||
static const auto RegExp = QRegularExpression("[^0-9]");
|
||||
return QString(parsed).replace(RegExp, QString());
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool UseMaskedField(FieldType type) {
|
||||
switch (type) {
|
||||
case FieldType::Text:
|
||||
case FieldType::Email:
|
||||
return false;
|
||||
case FieldType::CardNumber:
|
||||
case FieldType::CardExpireDate:
|
||||
case FieldType::CardCVC:
|
||||
case FieldType::Country:
|
||||
case FieldType::Phone:
|
||||
case FieldType::Money:
|
||||
return true;
|
||||
}
|
||||
Unexpected("FieldType in Payments::Ui::UseMaskedField.");
|
||||
}
|
||||
|
||||
[[nodiscard]] base::unique_qptr<RpWidget> CreateWrap(
|
||||
QWidget *parent,
|
||||
FieldConfig &config) {
|
||||
switch (config.type) {
|
||||
case FieldType::Text:
|
||||
case FieldType::Email:
|
||||
return base::make_unique_q<InputField>(
|
||||
parent,
|
||||
st::paymentsField,
|
||||
std::move(config.placeholder),
|
||||
Parse(config));
|
||||
case FieldType::CardNumber:
|
||||
case FieldType::CardExpireDate:
|
||||
case FieldType::CardCVC:
|
||||
case FieldType::Country:
|
||||
case FieldType::Phone:
|
||||
case FieldType::Money:
|
||||
return base::make_unique_q<RpWidget>(parent);
|
||||
}
|
||||
Unexpected("FieldType in Payments::Ui::CreateWrap.");
|
||||
}
|
||||
|
||||
[[nodiscard]] InputField *LookupInputField(
|
||||
not_null<RpWidget*> wrap,
|
||||
FieldConfig &config) {
|
||||
return UseMaskedField(config.type)
|
||||
? nullptr
|
||||
: static_cast<InputField*>(wrap.get());
|
||||
}
|
||||
|
||||
[[nodiscard]] MaskedInputField *CreateMoneyField(
|
||||
not_null<RpWidget*> wrap,
|
||||
FieldConfig &config,
|
||||
rpl::producer<> textPossiblyChanged) {
|
||||
struct State {
|
||||
CurrencyRule rule;
|
||||
style::InputField st;
|
||||
QString currencyText;
|
||||
int currencySkip = 0;
|
||||
FlatLabel *left = nullptr;
|
||||
FlatLabel *right = nullptr;
|
||||
};
|
||||
const auto state = wrap->lifetime().make_state<State>(State{
|
||||
.rule = LookupCurrencyRule(config.currency),
|
||||
.st = st::paymentsMoneyField,
|
||||
});
|
||||
const auto &rule = state->rule;
|
||||
state->currencySkip = rule.space ? state->st.style.font->spacew : 0;
|
||||
state->currencyText = ((!rule.left && rule.space)
|
||||
? QString(QChar(' '))
|
||||
: QString()) + (*rule.international
|
||||
? QString(rule.international)
|
||||
: config.currency) + ((rule.left && rule.space)
|
||||
? QString(QChar(' '))
|
||||
: QString());
|
||||
if (rule.left) {
|
||||
state->left = CreateChild<FlatLabel>(
|
||||
wrap.get(),
|
||||
state->currencyText,
|
||||
st::paymentsFieldAdditional);
|
||||
}
|
||||
state->right = CreateChild<FlatLabel>(
|
||||
wrap.get(),
|
||||
QString(),
|
||||
st::paymentsFieldAdditional);
|
||||
const auto leftSkip = state->left
|
||||
? (state->left->textMaxWidth() + state->currencySkip)
|
||||
: 0;
|
||||
const auto rightSkip = st::paymentsFieldAdditional.style.font->width(
|
||||
QString(QChar(rule.decimal))
|
||||
+ QString(QChar('0')).repeated(rule.exponent)
|
||||
+ (rule.left ? QString() : state->currencyText));
|
||||
state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);
|
||||
state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);
|
||||
const auto result = CreateChild<MaskedInputField>(
|
||||
wrap.get(),
|
||||
state->st,
|
||||
std::move(config.placeholder),
|
||||
Parse(config));
|
||||
result->setPlaceholderHidden(true);
|
||||
if (state->left) {
|
||||
state->left->move(0, state->st.textMargins.top());
|
||||
}
|
||||
const auto updateRight = [=] {
|
||||
const auto text = result->getLastText();
|
||||
const auto width = state->st.style.font->width(text);
|
||||
const auto &rule = state->rule;
|
||||
const auto symbol = QChar(rule.decimal);
|
||||
const auto decimal = text.indexOf(symbol);
|
||||
const auto zeros = (decimal >= 0)
|
||||
? std::max(rule.exponent - int(text.size() - decimal - 1), 0)
|
||||
: rule.stripDotZero
|
||||
? 0
|
||||
: rule.exponent;
|
||||
const auto valueDecimalSeparator = (decimal >= 0 || !zeros)
|
||||
? QString()
|
||||
: QString(symbol);
|
||||
const auto zeroString = QString(QChar('0'));
|
||||
const auto valueRightPart = (text.isEmpty() ? zeroString : QString())
|
||||
+ valueDecimalSeparator
|
||||
+ zeroString.repeated(zeros);
|
||||
const auto right = valueRightPart
|
||||
+ (rule.left ? QString() : state->currencyText);
|
||||
state->right->setText(right);
|
||||
state->right->setTextColorOverride(valueRightPart.isEmpty()
|
||||
? std::nullopt
|
||||
: std::make_optional(st::windowSubTextFg->c));
|
||||
state->right->move(
|
||||
(state->st.textMargins.left()
|
||||
+ width
|
||||
+ ((rule.left || !valueRightPart.isEmpty())
|
||||
? 0
|
||||
: state->currencySkip)),
|
||||
state->st.textMargins.top());
|
||||
};
|
||||
std::move(
|
||||
textPossiblyChanged
|
||||
) | rpl::on_next(updateRight, result->lifetime());
|
||||
if (state->left) {
|
||||
state->left->raise();
|
||||
}
|
||||
state->right->raise();
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] MaskedInputField *LookupMaskedField(
|
||||
not_null<RpWidget*> wrap,
|
||||
FieldConfig &config,
|
||||
rpl::producer<> textPossiblyChanged) {
|
||||
if (!UseMaskedField(config.type)) {
|
||||
return nullptr;
|
||||
}
|
||||
switch (config.type) {
|
||||
case FieldType::Text:
|
||||
case FieldType::Email:
|
||||
return nullptr;
|
||||
case FieldType::CardNumber:
|
||||
case FieldType::CardExpireDate:
|
||||
case FieldType::CardCVC:
|
||||
case FieldType::Country:
|
||||
return CreateChild<MaskedInputField>(
|
||||
wrap.get(),
|
||||
st::paymentsField,
|
||||
std::move(config.placeholder),
|
||||
Parse(config));
|
||||
case FieldType::Phone:
|
||||
return CreateChild<PhoneInput>(
|
||||
wrap.get(),
|
||||
st::paymentsField,
|
||||
std::move(config.placeholder),
|
||||
Countries::ExtractPhoneCode(config.defaultPhone),
|
||||
Parse(config),
|
||||
[](const QString &s) { return Countries::Groups(s); });
|
||||
case FieldType::Money:
|
||||
return CreateMoneyField(
|
||||
wrap,
|
||||
config,
|
||||
std::move(textPossiblyChanged));
|
||||
}
|
||||
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Field::Field(QWidget *parent, FieldConfig &&config)
|
||||
: _config(config)
|
||||
, _wrap(CreateWrap(parent, config))
|
||||
, _input(LookupInputField(_wrap.get(), config))
|
||||
, _masked(LookupMaskedField(
|
||||
_wrap.get(),
|
||||
config,
|
||||
_textPossiblyChanged.events_starting_with({})))
|
||||
, _countryIso2(config.value) {
|
||||
if (_masked) {
|
||||
setupMaskedGeometry();
|
||||
}
|
||||
if (_config.type == FieldType::Country) {
|
||||
setupCountry();
|
||||
}
|
||||
if (const auto &validator = config.validator) {
|
||||
setupValidator(validator);
|
||||
} else if (config.type == FieldType::Money) {
|
||||
setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
|
||||
}
|
||||
setupFrontBackspace();
|
||||
setupSubmit();
|
||||
}
|
||||
|
||||
RpWidget *Field::widget() const {
|
||||
return _wrap.get();
|
||||
}
|
||||
|
||||
object_ptr<RpWidget> Field::ownedWidget() const {
|
||||
return object_ptr<RpWidget>::fromRaw(_wrap.get());
|
||||
}
|
||||
|
||||
QString Field::value() const {
|
||||
return Format(
|
||||
_config,
|
||||
_input ? _input->getLastText() : _masked->getLastText(),
|
||||
_countryIso2);
|
||||
}
|
||||
|
||||
rpl::producer<> Field::frontBackspace() const {
|
||||
return _frontBackspace.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Field::finished() const {
|
||||
return _finished.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Field::submitted() const {
|
||||
return _submitted.events();
|
||||
}
|
||||
|
||||
void Field::setupMaskedGeometry() {
|
||||
Expects(_masked != nullptr);
|
||||
|
||||
_wrap->resize(_masked->size());
|
||||
_wrap->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_masked->resize(width, _masked->height());
|
||||
}, _masked->lifetime());
|
||||
_masked->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
_wrap->resize(_wrap->width(), height);
|
||||
}, _masked->lifetime());
|
||||
}
|
||||
|
||||
void Field::setupCountry() {
|
||||
Expects(_config.type == FieldType::Country);
|
||||
Expects(_masked != nullptr);
|
||||
|
||||
QObject::connect(_masked, &MaskedInputField::focused, [=] {
|
||||
setFocus();
|
||||
|
||||
const auto name = Countries::Instance().countryNameByISO2(
|
||||
_countryIso2);
|
||||
const auto country = !name.isEmpty()
|
||||
? _countryIso2
|
||||
: !_config.defaultCountry.isEmpty()
|
||||
? _config.defaultCountry
|
||||
: Platform::SystemCountry();
|
||||
auto box = Box<CountrySelectBox>(
|
||||
country,
|
||||
CountrySelectBox::Type::Countries);
|
||||
const auto raw = box.data();
|
||||
raw->countryChosen(
|
||||
) | rpl::on_next([=](QString iso2) {
|
||||
_countryIso2 = iso2;
|
||||
_masked->setText(Countries::Instance().countryNameByISO2(iso2));
|
||||
_masked->hideError();
|
||||
raw->closeBox();
|
||||
if (!iso2.isEmpty()) {
|
||||
if (_nextField) {
|
||||
_nextField->activate();
|
||||
} else {
|
||||
_submitted.fire({});
|
||||
}
|
||||
}
|
||||
}, _masked->lifetime());
|
||||
raw->boxClosing() | rpl::on_next([=] {
|
||||
setFocus();
|
||||
}, _masked->lifetime());
|
||||
_config.showBox(std::move(box));
|
||||
});
|
||||
}
|
||||
|
||||
void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
|
||||
Expects(validator != nullptr);
|
||||
|
||||
const auto state = [=]() -> State {
|
||||
if (_masked) {
|
||||
const auto position = _masked->cursorPosition();
|
||||
const auto selectionStart = _masked->selectionStart();
|
||||
const auto selectionEnd = _masked->selectionEnd();
|
||||
return {
|
||||
.value = _masked->getLastText(),
|
||||
.position = position,
|
||||
.anchor = (selectionStart == selectionEnd
|
||||
? position
|
||||
: (selectionStart == position)
|
||||
? selectionEnd
|
||||
: selectionStart),
|
||||
};
|
||||
}
|
||||
const auto cursor = _input->textCursor();
|
||||
return {
|
||||
.value = _input->getLastText(),
|
||||
.position = cursor.position(),
|
||||
.anchor = cursor.anchor(),
|
||||
};
|
||||
};
|
||||
const auto save = [=] {
|
||||
_was = state();
|
||||
};
|
||||
const auto setText = [=](const QString &text) {
|
||||
if (_masked) {
|
||||
_masked->setText(text);
|
||||
} else {
|
||||
_input->setText(text);
|
||||
}
|
||||
};
|
||||
const auto setPosition = [=](int position) {
|
||||
if (_masked) {
|
||||
_masked->setCursorPosition(position);
|
||||
} else {
|
||||
auto cursor = _input->textCursor();
|
||||
cursor.setPosition(position);
|
||||
_input->setTextCursor(cursor);
|
||||
}
|
||||
};
|
||||
const auto validate = [=] {
|
||||
if (_validating) {
|
||||
return;
|
||||
}
|
||||
_validating = true;
|
||||
const auto guard = gsl::finally([&] {
|
||||
_validating = false;
|
||||
save();
|
||||
_textPossiblyChanged.fire({});
|
||||
});
|
||||
|
||||
const auto now = state();
|
||||
const auto result = validator(ValidateRequest{
|
||||
.wasValue = _was.value,
|
||||
.wasPosition = _was.position,
|
||||
.wasAnchor = _was.anchor,
|
||||
.nowValue = now.value,
|
||||
.nowPosition = now.position,
|
||||
});
|
||||
_valid = result.finished || !result.invalid;
|
||||
|
||||
const auto changed = (result.value != now.value);
|
||||
if (changed) {
|
||||
setText(result.value);
|
||||
}
|
||||
if (changed || result.position != now.position) {
|
||||
setPosition(result.position);
|
||||
}
|
||||
if (result.finished) {
|
||||
_finished.fire({});
|
||||
} else if (result.invalid) {
|
||||
Ui::PostponeCall(
|
||||
_masked ? (QWidget*)_masked : _input,
|
||||
[=] { showErrorNoFocus(); });
|
||||
}
|
||||
};
|
||||
if (_masked) {
|
||||
QObject::connect(_masked, &QLineEdit::cursorPositionChanged, save);
|
||||
QObject::connect(_masked, &MaskedInputField::changed, validate);
|
||||
} else {
|
||||
const auto raw = _input->rawTextEdit();
|
||||
QObject::connect(raw, &QTextEdit::cursorPositionChanged, save);
|
||||
_input->changes(
|
||||
) | rpl::on_next(validate, _input->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
void Field::setupFrontBackspace() {
|
||||
const auto filter = [=](not_null<QEvent*> e) {
|
||||
const auto frontBackspace = (e->type() == QEvent::KeyPress)
|
||||
&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)
|
||||
&& (_masked
|
||||
? (_masked->cursorPosition() == 0
|
||||
&& _masked->selectionLength() == 0)
|
||||
: (_input->textCursor().position() == 0
|
||||
&& _input->textCursor().anchor() == 0));
|
||||
if (frontBackspace) {
|
||||
_frontBackspace.fire({});
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
};
|
||||
if (_masked) {
|
||||
base::install_event_filter(_masked, filter);
|
||||
} else {
|
||||
base::install_event_filter(_input->rawTextEdit(), filter);
|
||||
}
|
||||
}
|
||||
|
||||
void Field::setupSubmit() {
|
||||
const auto submitted = [=] {
|
||||
if (!_valid) {
|
||||
showError();
|
||||
} else if (_nextField) {
|
||||
_nextField->activate();
|
||||
} else {
|
||||
_submitted.fire({});
|
||||
}
|
||||
};
|
||||
if (_masked) {
|
||||
QObject::connect(_masked, &MaskedInputField::submitted, submitted);
|
||||
} else {
|
||||
_input->submits(
|
||||
) | rpl::on_next(submitted, _input->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
void Field::setNextField(not_null<Field*> field) {
|
||||
_nextField = field;
|
||||
|
||||
finished() | rpl::on_next([=] {
|
||||
field->setFocus();
|
||||
}, _masked ? _masked->lifetime() : _input->lifetime());
|
||||
}
|
||||
|
||||
void Field::setPreviousField(not_null<Field*> field) {
|
||||
frontBackspace(
|
||||
) | rpl::on_next([=] {
|
||||
field->setFocus();
|
||||
}, _masked ? _masked->lifetime() : _input->lifetime());
|
||||
}
|
||||
|
||||
void Field::activate() {
|
||||
if (_input) {
|
||||
_input->setFocus();
|
||||
} else {
|
||||
_masked->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void Field::setFocus() {
|
||||
if (_config.type == FieldType::Country) {
|
||||
_wrap->setFocus();
|
||||
} else {
|
||||
activate();
|
||||
}
|
||||
}
|
||||
|
||||
void Field::setFocusFast() {
|
||||
if (_config.type == FieldType::Country) {
|
||||
setFocus();
|
||||
} else if (_input) {
|
||||
_input->setFocusFast();
|
||||
} else {
|
||||
_masked->setFocusFast();
|
||||
}
|
||||
}
|
||||
|
||||
void Field::showError() {
|
||||
if (_config.type == FieldType::Country) {
|
||||
setFocus();
|
||||
_masked->showErrorNoFocus();
|
||||
} else if (_input) {
|
||||
_input->showError();
|
||||
} else {
|
||||
_masked->showError();
|
||||
}
|
||||
}
|
||||
|
||||
void Field::showErrorNoFocus() {
|
||||
if (_input) {
|
||||
_input->showErrorNoFocus();
|
||||
} else {
|
||||
_masked->showErrorNoFocus();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
143
Telegram/SourceFiles/payments/ui/payments_field.h
Normal file
143
Telegram/SourceFiles/payments/ui/payments_field.h
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class InputField;
|
||||
class MaskedInputField;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class FieldType {
|
||||
Text,
|
||||
CardNumber,
|
||||
CardExpireDate,
|
||||
CardCVC,
|
||||
Country,
|
||||
Phone,
|
||||
Email,
|
||||
Money,
|
||||
};
|
||||
|
||||
struct FieldValidateRequest {
|
||||
QString wasValue;
|
||||
int wasPosition = 0;
|
||||
int wasAnchor = 0;
|
||||
QString nowValue;
|
||||
int nowPosition = 0;
|
||||
};
|
||||
|
||||
struct FieldValidateResult {
|
||||
QString value;
|
||||
int position = 0;
|
||||
bool invalid = false;
|
||||
bool finished = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline auto RangeLengthValidator(int minLength, int maxLength) {
|
||||
return [=](FieldValidateRequest request) {
|
||||
return FieldValidateResult{
|
||||
.value = request.nowValue,
|
||||
.position = request.nowPosition,
|
||||
.invalid = (request.nowValue.size() < minLength
|
||||
|| request.nowValue.size() > maxLength),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto MaxLengthValidator(int maxLength) {
|
||||
return RangeLengthValidator(0, maxLength);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto RequiredValidator() {
|
||||
return RangeLengthValidator(1, std::numeric_limits<int>::max());
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto RequiredFinishedValidator() {
|
||||
return [=](FieldValidateRequest request) {
|
||||
return FieldValidateResult{
|
||||
.value = request.nowValue,
|
||||
.position = request.nowPosition,
|
||||
.invalid = request.nowValue.isEmpty(),
|
||||
.finished = !request.nowValue.isEmpty(),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
struct FieldConfig {
|
||||
FieldType type = FieldType::Text;
|
||||
rpl::producer<QString> placeholder;
|
||||
QString value;
|
||||
Fn<FieldValidateResult(FieldValidateRequest)> validator;
|
||||
Fn<void(object_ptr<BoxContent>)> showBox;
|
||||
QString currency;
|
||||
QString defaultPhone;
|
||||
QString defaultCountry;
|
||||
};
|
||||
|
||||
class Field final {
|
||||
public:
|
||||
Field(QWidget *parent, FieldConfig &&config);
|
||||
|
||||
[[nodiscard]] RpWidget *widget() const;
|
||||
[[nodiscard]] object_ptr<RpWidget> ownedWidget() const;
|
||||
|
||||
[[nodiscard]] QString value() const;
|
||||
[[nodiscard]] rpl::producer<> frontBackspace() const;
|
||||
[[nodiscard]] rpl::producer<> finished() const;
|
||||
[[nodiscard]] rpl::producer<> submitted() const;
|
||||
|
||||
void activate();
|
||||
void setFocus();
|
||||
void setFocusFast();
|
||||
void showError();
|
||||
void showErrorNoFocus();
|
||||
|
||||
void setNextField(not_null<Field*> field);
|
||||
void setPreviousField(not_null<Field*> field);
|
||||
|
||||
private:
|
||||
struct State {
|
||||
QString value;
|
||||
int position = 0;
|
||||
int anchor = 0;
|
||||
};
|
||||
using ValidateRequest = FieldValidateRequest;
|
||||
using ValidateResult = FieldValidateResult;
|
||||
|
||||
void setupMaskedGeometry();
|
||||
void setupCountry();
|
||||
void setupValidator(Fn<ValidateResult(ValidateRequest)> validator);
|
||||
void setupFrontBackspace();
|
||||
void setupSubmit();
|
||||
|
||||
const FieldConfig _config;
|
||||
const base::unique_qptr<RpWidget> _wrap;
|
||||
rpl::event_stream<> _frontBackspace;
|
||||
rpl::event_stream<> _finished;
|
||||
rpl::event_stream<> _submitted;
|
||||
rpl::event_stream<> _textPossiblyChanged; // Must be above _masked.
|
||||
InputField *_input = nullptr;
|
||||
MaskedInputField *_masked = nullptr;
|
||||
Field *_nextField = nullptr;
|
||||
QString _countryIso2;
|
||||
State _was;
|
||||
bool _validating = false;
|
||||
bool _valid = true;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
612
Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
Normal file
612
Telegram/SourceFiles/payments/ui/payments_form_summary.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "payments/ui/payments_form_summary.h"
|
||||
|
||||
#include "payments/ui/payments_panel_delegate.h"
|
||||
#include "settings/settings_common.h" // AddButtonWithLabel.
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "styles/style_payments.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLightOpacity = 0.1;
|
||||
constexpr auto kLightRippleOpacity = 0.11;
|
||||
constexpr auto kChosenOpacity = 0.8;
|
||||
constexpr auto kChosenRippleOpacity = 0.5;
|
||||
|
||||
[[nodiscard]] Fn<QColor()> TransparentColor(
|
||||
const style::color &c,
|
||||
float64 opacity) {
|
||||
return [&c, opacity] {
|
||||
return QColor(
|
||||
c->c.red(),
|
||||
c->c.green(),
|
||||
c->c.blue(),
|
||||
c->c.alpha() * opacity);
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] style::RoundButton TipButtonStyle(
|
||||
const style::RoundButton &original,
|
||||
const style::color &light,
|
||||
const style::color &ripple) {
|
||||
auto result = original;
|
||||
result.textBg = light;
|
||||
result.ripple.color = ripple;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class PanelDelegate;
|
||||
|
||||
FormSummary::FormSummary(
|
||||
QWidget *parent,
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options,
|
||||
not_null<PanelDelegate*> delegate,
|
||||
int scrollTop)
|
||||
: _delegate(delegate)
|
||||
, _invoice(invoice)
|
||||
, _method(method)
|
||||
, _options(options)
|
||||
, _information(current)
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _layout(_scroll->setOwnedWidget(object_ptr<VerticalLayout>(this)))
|
||||
, _topShadow(this)
|
||||
, _bottomShadow(this)
|
||||
, _submit(_invoice.receipt.paid
|
||||
? object_ptr<RoundButton>(nullptr)
|
||||
: object_ptr<RoundButton>(
|
||||
this,
|
||||
tr::lng_payments_pay_amount(
|
||||
lt_amount,
|
||||
rpl::single(formatAmount(computeTotalAmount()))),
|
||||
st::paymentsPanelSubmit))
|
||||
, _cancel(
|
||||
this,
|
||||
(_invoice.receipt.paid
|
||||
? tr::lng_about_done()
|
||||
: tr::lng_cancel()),
|
||||
st::paymentsPanelButton)
|
||||
, _tipLightBg(TransparentColor(st::paymentsTipActive, kLightOpacity))
|
||||
, _tipLightRipple(
|
||||
TransparentColor(st::paymentsTipActive, kLightRippleOpacity))
|
||||
, _tipChosenBg(TransparentColor(st::paymentsTipActive, kChosenOpacity))
|
||||
, _tipChosenRipple(
|
||||
TransparentColor(st::paymentsTipActive, kChosenRippleOpacity))
|
||||
, _tipButton(TipButtonStyle(
|
||||
st::paymentsTipButton,
|
||||
_tipLightBg.color(),
|
||||
_tipLightRipple.color()))
|
||||
, _tipChosen(TipButtonStyle(
|
||||
st::paymentsTipChosen,
|
||||
_tipChosenBg.color(),
|
||||
_tipChosenRipple.color()))
|
||||
, _initialScrollTop(scrollTop) {
|
||||
setupControls();
|
||||
}
|
||||
|
||||
rpl::producer<int> FormSummary::scrollTopValue() const {
|
||||
return _scroll->scrollTopValue();
|
||||
}
|
||||
|
||||
bool FormSummary::showCriticalError(const TextWithEntities &text) {
|
||||
if (_invoice
|
||||
|| (_scroll->height() - _layout->height()
|
||||
< st::paymentsPanelSize.height() / 2)) {
|
||||
return false;
|
||||
}
|
||||
Ui::AddSkip(_layout.get(), st::paymentsPricesTopSkip);
|
||||
_layout->add(
|
||||
object_ptr<FlatLabel>(
|
||||
_layout.get(),
|
||||
rpl::single(text),
|
||||
st::paymentsCriticalError),
|
||||
style::al_top);
|
||||
return true;
|
||||
}
|
||||
|
||||
int FormSummary::contentHeight() const {
|
||||
return _invoice ? _scroll->height() : _layout->height();
|
||||
}
|
||||
|
||||
void FormSummary::updateThumbnail(const QImage &thumbnail) {
|
||||
_invoice.cover.thumbnail = thumbnail;
|
||||
_thumbnails.fire_copy(thumbnail);
|
||||
}
|
||||
|
||||
QString FormSummary::formatAmount(
|
||||
int64 amount,
|
||||
bool forceStripDotZero) const {
|
||||
return FillAmountAndCurrency(
|
||||
amount,
|
||||
_invoice.currency,
|
||||
forceStripDotZero);
|
||||
}
|
||||
|
||||
int64 FormSummary::computeTotalAmount() const {
|
||||
const auto total = ranges::accumulate(
|
||||
_invoice.prices,
|
||||
int64(0),
|
||||
std::plus<>(),
|
||||
&LabeledPrice::price);
|
||||
const auto selected = ranges::find(
|
||||
_options.list,
|
||||
_options.selectedId,
|
||||
&ShippingOption::id);
|
||||
const auto shipping = (selected != end(_options.list))
|
||||
? ranges::accumulate(
|
||||
selected->prices,
|
||||
int64(0),
|
||||
std::plus<>(),
|
||||
&LabeledPrice::price)
|
||||
: int64(0);
|
||||
return total + shipping + _invoice.tipsSelected;
|
||||
}
|
||||
|
||||
void FormSummary::setupControls() {
|
||||
setupContent(_layout.get());
|
||||
|
||||
if (_submit) {
|
||||
_submit->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
_submit->addClickHandler([=] {
|
||||
_delegate->panelSubmit();
|
||||
});
|
||||
}
|
||||
_cancel->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
_cancel->addClickHandler([=] {
|
||||
_delegate->panelRequestClose();
|
||||
});
|
||||
if (!_invoice) {
|
||||
if (_submit) {
|
||||
_submit->hide();
|
||||
}
|
||||
_cancel->hide();
|
||||
}
|
||||
|
||||
using namespace rpl::mappers;
|
||||
|
||||
_topShadow->toggleOn(
|
||||
_scroll->scrollTopValue() | rpl::map(_1 > 0));
|
||||
_bottomShadow->toggleOn(rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue(),
|
||||
_layout->heightValue(),
|
||||
_1 + _2 < _3));
|
||||
|
||||
rpl::merge(
|
||||
(_submit ? _submit->widthValue() : rpl::single(0)),
|
||||
_cancel->widthValue()
|
||||
) | rpl::skip(2) | rpl::on_next([=] {
|
||||
updateControlsGeometry();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void FormSummary::setupCover(not_null<VerticalLayout*> layout) {
|
||||
struct State {
|
||||
QImage thumbnail;
|
||||
FlatLabel *title = nullptr;
|
||||
FlatLabel *description = nullptr;
|
||||
FlatLabel *seller = nullptr;
|
||||
};
|
||||
const auto cover = layout->add(object_ptr<RpWidget>(layout));
|
||||
const auto state = cover->lifetime().make_state<State>();
|
||||
state->title = CreateChild<FlatLabel>(
|
||||
cover,
|
||||
_invoice.cover.title,
|
||||
st::paymentsTitle);
|
||||
state->description = CreateChild<FlatLabel>(
|
||||
cover,
|
||||
rpl::single(_invoice.cover.description),
|
||||
st::paymentsDescription);
|
||||
state->seller = CreateChild<FlatLabel>(
|
||||
cover,
|
||||
_invoice.cover.seller,
|
||||
st::paymentsSeller);
|
||||
cover->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
if (state->thumbnail.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto &padding = st::paymentsCoverPadding;
|
||||
const auto left = padding.left();
|
||||
const auto top = padding.top();
|
||||
const auto rect = QRect(
|
||||
QPoint(left, top),
|
||||
state->thumbnail.size() / state->thumbnail.devicePixelRatio());
|
||||
if (rect.intersects(clip)) {
|
||||
QPainter(cover).drawImage(rect, state->thumbnail);
|
||||
}
|
||||
}, cover->lifetime());
|
||||
rpl::combine(
|
||||
cover->widthValue(),
|
||||
_thumbnails.events_starting_with_copy(_invoice.cover.thumbnail)
|
||||
) | rpl::on_next([=](int width, QImage &&thumbnail) {
|
||||
const auto &padding = st::paymentsCoverPadding;
|
||||
const auto thumbnailSkip = st::paymentsThumbnailSize.width()
|
||||
+ st::paymentsThumbnailSkip;
|
||||
const auto left = padding.left()
|
||||
+ (thumbnail.isNull() ? 0 : thumbnailSkip);
|
||||
const auto available = width
|
||||
- padding.left()
|
||||
- padding.right()
|
||||
- (thumbnail.isNull() ? 0 : thumbnailSkip);
|
||||
state->title->resizeToNaturalWidth(available);
|
||||
state->title->moveToLeft(
|
||||
left,
|
||||
padding.top() + st::paymentsTitleTop);
|
||||
state->description->resizeToNaturalWidth(available);
|
||||
state->description->moveToLeft(
|
||||
left,
|
||||
(state->title->y()
|
||||
+ state->title->height()
|
||||
+ st::paymentsDescriptionTop));
|
||||
state->seller->resizeToNaturalWidth(available);
|
||||
state->seller->moveToLeft(
|
||||
left,
|
||||
(state->description->y()
|
||||
+ state->description->height()
|
||||
+ st::paymentsSellerTop));
|
||||
const auto thumbnailHeight = padding.top()
|
||||
+ (thumbnail.isNull()
|
||||
? 0
|
||||
: int(thumbnail.height() / thumbnail.devicePixelRatio()))
|
||||
+ padding.bottom();
|
||||
const auto height = state->seller->y()
|
||||
+ state->seller->height()
|
||||
+ padding.bottom();
|
||||
cover->resize(width, std::max(thumbnailHeight, height));
|
||||
state->thumbnail = std::move(thumbnail);
|
||||
cover->update();
|
||||
}, cover->lifetime());
|
||||
}
|
||||
|
||||
void FormSummary::setupPrices(not_null<VerticalLayout*> layout) {
|
||||
const auto addRow = [&](
|
||||
const QString &label,
|
||||
const TextWithEntities &value,
|
||||
bool full = false) {
|
||||
const auto &st = full
|
||||
? st::paymentsFullPriceAmount
|
||||
: st::paymentsPriceAmount;
|
||||
const auto right = CreateChild<FlatLabel>(
|
||||
layout.get(),
|
||||
rpl::single(value),
|
||||
st);
|
||||
const auto &padding = st::paymentsPricePadding;
|
||||
const auto left = layout->add(
|
||||
object_ptr<FlatLabel>(
|
||||
layout,
|
||||
label,
|
||||
(full
|
||||
? st::paymentsFullPriceLabel
|
||||
: st::paymentsPriceLabel)),
|
||||
style::margins(
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
(padding.right()
|
||||
+ right->textMaxWidth()
|
||||
+ 2 * st.style.font->spacew),
|
||||
padding.bottom()));
|
||||
rpl::combine(
|
||||
left->topValue(),
|
||||
layout->widthValue()
|
||||
) | rpl::on_next([=](int top, int width) {
|
||||
right->moveToRight(st::paymentsPricePadding.right(), top, width);
|
||||
}, right->lifetime());
|
||||
return right;
|
||||
};
|
||||
|
||||
Ui::AddSkip(layout, st::paymentsPricesTopSkip);
|
||||
if (_invoice.receipt) {
|
||||
addRow(
|
||||
tr::lng_payments_date_label(tr::now),
|
||||
{ langDateTime(base::unixtime::parse(_invoice.receipt.date)) },
|
||||
true);
|
||||
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
|
||||
Ui::AddDivider(layout);
|
||||
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
|
||||
}
|
||||
|
||||
const auto add = [&](
|
||||
const QString &label,
|
||||
int64 amount,
|
||||
bool full = false) {
|
||||
addRow(label, { formatAmount(amount) }, full);
|
||||
};
|
||||
for (const auto &price : _invoice.prices) {
|
||||
add(price.label, price.price);
|
||||
}
|
||||
const auto selected = ranges::find(
|
||||
_options.list,
|
||||
_options.selectedId,
|
||||
&ShippingOption::id);
|
||||
if (selected != end(_options.list)) {
|
||||
for (const auto &price : selected->prices) {
|
||||
add(price.label, price.price);
|
||||
}
|
||||
}
|
||||
|
||||
const auto computedTotal = computeTotalAmount();
|
||||
const auto total = _invoice.receipt.paid
|
||||
? _invoice.receipt.totalAmount
|
||||
: computedTotal;
|
||||
if (_invoice.receipt.paid) {
|
||||
if (const auto tips = total - computedTotal) {
|
||||
add(tr::lng_payments_tips_label(tr::now), tips);
|
||||
}
|
||||
} else if (_invoice.tipsMax > 0) {
|
||||
const auto text = formatAmount(_invoice.tipsSelected);
|
||||
const auto label = addRow(
|
||||
tr::lng_payments_tips_label(tr::now),
|
||||
tr::link(text));
|
||||
label->overrideLinkClickHandler([=] {
|
||||
_delegate->panelChooseTips();
|
||||
});
|
||||
setupSuggestedTips(layout);
|
||||
}
|
||||
|
||||
add(tr::lng_payments_total_label(tr::now), total, true);
|
||||
Ui::AddSkip(layout, st::paymentsPricesBottomSkip);
|
||||
}
|
||||
|
||||
void FormSummary::setupSuggestedTips(not_null<VerticalLayout*> layout) {
|
||||
if (_invoice.suggestedTips.empty()) {
|
||||
return;
|
||||
}
|
||||
struct Button {
|
||||
RoundButton *widget = nullptr;
|
||||
int minWidth = 0;
|
||||
};
|
||||
struct State {
|
||||
std::vector<Button> buttons;
|
||||
int maxWidth = 0;
|
||||
};
|
||||
const auto outer = layout->add(
|
||||
object_ptr<RpWidget>(layout),
|
||||
st::paymentsTipButtonsPadding);
|
||||
const auto state = outer->lifetime().make_state<State>();
|
||||
for (const auto amount : _invoice.suggestedTips) {
|
||||
const auto selected = (amount == _invoice.tipsSelected);
|
||||
const auto &st = selected
|
||||
? _tipChosen
|
||||
: _tipButton;
|
||||
state->buttons.push_back(Button{
|
||||
.widget = CreateChild<RoundButton>(
|
||||
outer,
|
||||
rpl::single(formatAmount(amount, true)),
|
||||
st),
|
||||
});
|
||||
auto &button = state->buttons.back();
|
||||
button.widget->show();
|
||||
button.widget->setClickedCallback([=] {
|
||||
_delegate->panelChangeTips(selected ? 0 : amount);
|
||||
});
|
||||
button.minWidth = button.widget->width();
|
||||
state->maxWidth = std::max(state->maxWidth, button.minWidth);
|
||||
}
|
||||
outer->widthValue(
|
||||
) | rpl::filter([=](int outerWidth) {
|
||||
return outerWidth >= state->maxWidth;
|
||||
}) | rpl::on_next([=](int outerWidth) {
|
||||
const auto skip = st::paymentsTipSkip;
|
||||
const auto &buttons = state->buttons;
|
||||
auto left = outerWidth;
|
||||
auto height = 0;
|
||||
auto rowStart = 0;
|
||||
auto rowEnd = 0;
|
||||
auto buttonWidths = std::vector<float64>();
|
||||
const auto layoutRow = [&] {
|
||||
const auto count = rowEnd - rowStart;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
buttonWidths.resize(count);
|
||||
ranges::fill(buttonWidths, 0.);
|
||||
auto available = float64(outerWidth - (count - 1) * skip);
|
||||
auto zeros = count;
|
||||
do {
|
||||
const auto started = zeros;
|
||||
const auto average = available / zeros;
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
if (buttonWidths[i] > 0.) {
|
||||
continue;
|
||||
}
|
||||
const auto min = buttons[rowStart + i].minWidth;
|
||||
if (min > average) {
|
||||
buttonWidths[i] = min;
|
||||
available -= min;
|
||||
--zeros;
|
||||
}
|
||||
}
|
||||
if (started == zeros) {
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
if (!buttonWidths[i]) {
|
||||
buttonWidths[i] = average;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (zeros > 0);
|
||||
auto x = 0.;
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto button = buttons[rowStart + i].widget;
|
||||
auto right = x + buttonWidths[i];
|
||||
button->setFullWidth(
|
||||
int(base::SafeRound(right) - base::SafeRound(x)));
|
||||
button->moveToLeft(
|
||||
int(base::SafeRound(x)),
|
||||
height,
|
||||
outerWidth);
|
||||
x = right + skip;
|
||||
}
|
||||
height += buttons[0].widget->height() + skip;
|
||||
};
|
||||
for (const auto &button : buttons) {
|
||||
if (button.minWidth <= left) {
|
||||
left -= button.minWidth + skip;
|
||||
++rowEnd;
|
||||
continue;
|
||||
}
|
||||
layoutRow();
|
||||
rowStart = rowEnd++;
|
||||
left = outerWidth - button.minWidth - skip;
|
||||
}
|
||||
layoutRow();
|
||||
outer->resize(outerWidth, height - skip);
|
||||
}, outer->lifetime());
|
||||
}
|
||||
|
||||
void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
|
||||
Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
|
||||
|
||||
const auto add = [&](
|
||||
rpl::producer<QString> title,
|
||||
const QString &label,
|
||||
const style::icon *icon,
|
||||
Fn<void()> handler) {
|
||||
const auto button = Settings::AddButtonWithLabel(
|
||||
layout,
|
||||
std::move(title),
|
||||
rpl::single(label),
|
||||
st::paymentsSectionButton,
|
||||
{ .icon = icon });
|
||||
button->addClickHandler(std::move(handler));
|
||||
if (_invoice.receipt) {
|
||||
button->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
};
|
||||
add(
|
||||
tr::lng_payments_payment_method(),
|
||||
(_method.savedMethods.empty()
|
||||
? QString()
|
||||
: _method.savedMethods[_method.savedMethodIndex].title),
|
||||
&st::paymentsIconPaymentMethod,
|
||||
[=] { _delegate->panelEditPaymentMethod(); });
|
||||
if (_invoice.isShippingAddressRequested) {
|
||||
auto list = QStringList();
|
||||
const auto push = [&](const QString &value) {
|
||||
if (!value.isEmpty()) {
|
||||
list.push_back(value);
|
||||
}
|
||||
};
|
||||
push(_information.shippingAddress.address1);
|
||||
push(_information.shippingAddress.address2);
|
||||
push(_information.shippingAddress.city);
|
||||
push(_information.shippingAddress.state);
|
||||
push(Countries::Instance().countryNameByISO2(
|
||||
_information.shippingAddress.countryIso2));
|
||||
push(_information.shippingAddress.postcode);
|
||||
add(
|
||||
tr::lng_payments_shipping_address(),
|
||||
list.join(", "),
|
||||
&st::paymentsIconShippingAddress,
|
||||
[=] { _delegate->panelEditShippingInformation(); });
|
||||
}
|
||||
if (!_options.list.empty()) {
|
||||
const auto selected = ranges::find(
|
||||
_options.list,
|
||||
_options.selectedId,
|
||||
&ShippingOption::id);
|
||||
add(
|
||||
tr::lng_payments_shipping_method(),
|
||||
(selected != end(_options.list)) ? selected->title : QString(),
|
||||
&st::paymentsIconShippingMethod,
|
||||
[=] { _delegate->panelChooseShippingOption(); });
|
||||
}
|
||||
if (_invoice.isNameRequested) {
|
||||
add(
|
||||
tr::lng_payments_info_name(),
|
||||
_information.name,
|
||||
&st::paymentsIconName,
|
||||
[=] { _delegate->panelEditName(); });
|
||||
}
|
||||
if (_invoice.isEmailRequested) {
|
||||
add(
|
||||
tr::lng_payments_info_email(),
|
||||
_information.email,
|
||||
&st::paymentsIconEmail,
|
||||
[=] { _delegate->panelEditEmail(); });
|
||||
}
|
||||
if (_invoice.isPhoneRequested) {
|
||||
add(
|
||||
tr::lng_payments_info_phone(),
|
||||
(_information.phone.isEmpty()
|
||||
? QString()
|
||||
: Ui::FormatPhone(_information.phone)),
|
||||
&st::paymentsIconPhone,
|
||||
[=] { _delegate->panelEditPhone(); });
|
||||
}
|
||||
Ui::AddSkip(layout, st::paymentsSectionsTopSkip);
|
||||
}
|
||||
|
||||
void FormSummary::setupContent(not_null<VerticalLayout*> layout) {
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
layout->resizeToWidth(width);
|
||||
}, layout->lifetime());
|
||||
|
||||
setupCover(layout);
|
||||
if (_invoice) {
|
||||
Ui::AddDivider(layout);
|
||||
setupPrices(layout);
|
||||
Ui::AddDivider(layout);
|
||||
setupSections(layout);
|
||||
}
|
||||
}
|
||||
|
||||
void FormSummary::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void FormSummary::updateControlsGeometry() {
|
||||
const auto &padding = st::paymentsPanelPadding;
|
||||
const auto buttonsHeight = padding.top()
|
||||
+ _cancel->height()
|
||||
+ padding.bottom();
|
||||
const auto buttonsTop = height() - buttonsHeight;
|
||||
_scroll->setGeometry(0, 0, width(), buttonsTop);
|
||||
_topShadow->resizeToWidth(width());
|
||||
_topShadow->moveToLeft(0, 0);
|
||||
_bottomShadow->resizeToWidth(width());
|
||||
_bottomShadow->moveToLeft(0, buttonsTop - st::lineWidth);
|
||||
auto right = padding.right();
|
||||
if (_submit) {
|
||||
_submit->moveToRight(right, buttonsTop + padding.top());
|
||||
right += _submit->width() + padding.left();
|
||||
}
|
||||
_cancel->moveToRight(right, buttonsTop + padding.top());
|
||||
|
||||
_scroll->updateBars();
|
||||
|
||||
if (buttonsTop > 0 && width() > 0) {
|
||||
if (const auto top = base::take(_initialScrollTop)) {
|
||||
_scroll->scrollToY(top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
84
Telegram/SourceFiles/payments/ui/payments_form_summary.h
Normal file
84
Telegram/SourceFiles/payments/ui/payments_form_summary.h
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "payments/ui/payments_panel_data.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class FadeShadow;
|
||||
class RoundButton;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class PanelDelegate;
|
||||
|
||||
class FormSummary final : public RpWidget {
|
||||
public:
|
||||
FormSummary(
|
||||
QWidget *parent,
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options,
|
||||
not_null<PanelDelegate*> delegate,
|
||||
int scrollTop);
|
||||
|
||||
void updateThumbnail(const QImage &thumbnail);
|
||||
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
|
||||
|
||||
bool showCriticalError(const TextWithEntities &text);
|
||||
[[nodiscard]] int contentHeight() const;
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void setupControls();
|
||||
void setupContent(not_null<VerticalLayout*> layout);
|
||||
void setupCover(not_null<VerticalLayout*> layout);
|
||||
void setupPrices(not_null<VerticalLayout*> layout);
|
||||
void setupSuggestedTips(not_null<VerticalLayout*> layout);
|
||||
void setupSections(not_null<VerticalLayout*> layout);
|
||||
void updateControlsGeometry();
|
||||
|
||||
[[nodiscard]] QString formatAmount(
|
||||
int64 amount,
|
||||
bool forceStripDotZero = false) const;
|
||||
[[nodiscard]] int64 computeTotalAmount() const;
|
||||
|
||||
const not_null<PanelDelegate*> _delegate;
|
||||
Invoice _invoice;
|
||||
PaymentMethodDetails _method;
|
||||
ShippingOptions _options;
|
||||
RequestedInformation _information;
|
||||
object_ptr<ScrollArea> _scroll;
|
||||
not_null<VerticalLayout*> _layout;
|
||||
object_ptr<FadeShadow> _topShadow;
|
||||
object_ptr<FadeShadow> _bottomShadow;
|
||||
object_ptr<RoundButton> _submit;
|
||||
object_ptr<RoundButton> _cancel;
|
||||
rpl::event_stream<QImage> _thumbnails;
|
||||
|
||||
style::complex_color _tipLightBg;
|
||||
style::complex_color _tipLightRipple;
|
||||
style::complex_color _tipChosenBg;
|
||||
style::complex_color _tipChosenRipple;
|
||||
style::RoundButton _tipButton;
|
||||
style::RoundButton _tipChosen;
|
||||
int _initialScrollTop = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
949
Telegram/SourceFiles/payments/ui/payments_panel.cpp
Normal file
949
Telegram/SourceFiles/payments/ui/payments_panel.cpp
Normal file
@@ -0,0 +1,949 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "payments/ui/payments_panel.h"
|
||||
|
||||
#include "payments/ui/payments_form_summary.h"
|
||||
#include "payments/ui/payments_edit_information.h"
|
||||
#include "payments/ui/payments_edit_card.h"
|
||||
#include "payments/ui/payments_panel_delegate.h"
|
||||
#include "payments/ui/payments_field.h"
|
||||
#include "ui/widgets/separate_panel.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/boxes/single_choice_box.h"
|
||||
#include "ui/chat/attach/attach_bot_webview.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "webview/webview_embed.h"
|
||||
#include "webview/webview_interface.h"
|
||||
#include "styles/style_payments.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Payments::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kProgressDuration = crl::time(200);
|
||||
constexpr auto kProgressOpacity = 0.3;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Panel::Progress {
|
||||
Progress(QWidget *parent, Fn<QRect()> rect);
|
||||
|
||||
RpWidget widget;
|
||||
InfiniteRadialAnimation animation;
|
||||
Animations::Simple shownAnimation;
|
||||
bool shown = true;
|
||||
rpl::lifetime geometryLifetime;
|
||||
};
|
||||
|
||||
struct Panel::WebviewWithLifetime {
|
||||
WebviewWithLifetime(
|
||||
QWidget *parent = nullptr,
|
||||
Webview::WindowConfig config = Webview::WindowConfig());
|
||||
|
||||
Webview::Window window;
|
||||
QPointer<RpWidget> lastHidingBox;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
Panel::WebviewWithLifetime::WebviewWithLifetime(
|
||||
QWidget *parent,
|
||||
Webview::WindowConfig config)
|
||||
: window(parent, std::move(config)) {
|
||||
}
|
||||
|
||||
Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
|
||||
: widget(parent)
|
||||
, animation(
|
||||
[=] { if (!anim::Disabled()) widget.update(rect()); },
|
||||
st::paymentsLoading) {
|
||||
}
|
||||
|
||||
Panel::Panel(not_null<PanelDelegate*> delegate)
|
||||
: _delegate(delegate)
|
||||
, _widget(std::make_unique<SeparatePanel>()) {
|
||||
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
|
||||
_widget->setInnerSize(st::paymentsPanelSize);
|
||||
|
||||
_widget->closeRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_delegate->panelRequestClose();
|
||||
}, _widget->lifetime());
|
||||
|
||||
_widget->closeEvents(
|
||||
) | rpl::on_next([=] {
|
||||
_delegate->panelCloseSure();
|
||||
}, _widget->lifetime());
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::filter([=] {
|
||||
return !_themeUpdateScheduled;
|
||||
}) | rpl::on_next([=] {
|
||||
_themeUpdateScheduled = true;
|
||||
crl::on_main(_widget.get(), [=] {
|
||||
_themeUpdateScheduled = false;
|
||||
updateThemeParams(_delegate->panelWebviewThemeParams());
|
||||
});
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
Panel::~Panel() {
|
||||
base::take(_webview);
|
||||
_progress = nullptr;
|
||||
_widget = nullptr;
|
||||
}
|
||||
|
||||
void Panel::requestActivate() {
|
||||
_widget->showAndActivate();
|
||||
}
|
||||
|
||||
void Panel::toggleProgress(bool shown) {
|
||||
if (!_progress) {
|
||||
if (!shown) {
|
||||
return;
|
||||
}
|
||||
_progress = std::make_unique<Progress>(
|
||||
_widget.get(),
|
||||
[=] { return progressRect(); });
|
||||
_progress->widget.paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
auto p = QPainter(&_progress->widget);
|
||||
p.setOpacity(
|
||||
_progress->shownAnimation.value(_progress->shown ? 1. : 0.));
|
||||
auto thickness = st::paymentsLoading.thickness;
|
||||
if (progressWithBackground()) {
|
||||
auto color = st::windowBg->c;
|
||||
color.setAlphaF(kProgressOpacity);
|
||||
p.fillRect(clip, color);
|
||||
}
|
||||
const auto rect = progressRect().marginsRemoved(
|
||||
{ thickness, thickness, thickness, thickness });
|
||||
InfiniteRadialAnimation::Draw(
|
||||
p,
|
||||
_progress->animation.computeState(),
|
||||
rect.topLeft(),
|
||||
rect.size() - QSize(),
|
||||
_progress->widget.width(),
|
||||
st::paymentsLoading.color,
|
||||
thickness);
|
||||
}, _progress->widget.lifetime());
|
||||
_progress->widget.show();
|
||||
_progress->animation.start();
|
||||
} else if (_progress->shown == shown) {
|
||||
return;
|
||||
}
|
||||
const auto callback = [=] {
|
||||
if (!_progress->shownAnimation.animating() && !_progress->shown) {
|
||||
_progress = nullptr;
|
||||
} else {
|
||||
_progress->widget.update();
|
||||
}
|
||||
};
|
||||
_progress->shown = shown;
|
||||
_progress->shownAnimation.start(
|
||||
callback,
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kProgressDuration);
|
||||
if (shown) {
|
||||
setupProgressGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
bool Panel::progressWithBackground() const {
|
||||
return (_progress->widget.width() == _widget->innerGeometry().width());
|
||||
}
|
||||
|
||||
QRect Panel::progressRect() const {
|
||||
const auto rect = _progress->widget.rect();
|
||||
if (!progressWithBackground()) {
|
||||
return rect;
|
||||
}
|
||||
const auto size = st::defaultBoxButton.height;
|
||||
return QRect(
|
||||
rect.x() + (rect.width() - size) / 2,
|
||||
rect.y() + (rect.height() - size) / 2,
|
||||
size,
|
||||
size);
|
||||
}
|
||||
|
||||
void Panel::setupProgressGeometry() {
|
||||
if (!_progress || !_progress->shown) {
|
||||
return;
|
||||
}
|
||||
_progress->geometryLifetime.destroy();
|
||||
if (_webviewBottom) {
|
||||
_webviewBottom->geometryValue(
|
||||
) | rpl::on_next([=](QRect bottom) {
|
||||
const auto height = bottom.height();
|
||||
const auto size = st::paymentsLoading.size;
|
||||
const auto skip = (height - size.height()) / 2;
|
||||
const auto inner = _widget->innerGeometry();
|
||||
const auto right = inner.x() + inner.width();
|
||||
const auto top = inner.y() + inner.height() - height;
|
||||
// This doesn't work, because first we get the correct bottom
|
||||
// geometry and after that we get the previous event (which
|
||||
// triggered the 'fire' of correct geometry before getting here).
|
||||
//const auto right = bottom.x() + bottom.width();
|
||||
//const auto top = bottom.y();
|
||||
_progress->widget.setGeometry(QRect{
|
||||
QPoint(right - skip - size.width(), top + skip),
|
||||
size });
|
||||
}, _progress->geometryLifetime);
|
||||
} else if (_weakFormSummary) {
|
||||
_weakFormSummary->sizeValue(
|
||||
) | rpl::on_next([=](QSize form) {
|
||||
const auto full = _widget->innerGeometry();
|
||||
const auto size = st::defaultBoxButton.height;
|
||||
const auto inner = _weakFormSummary->contentHeight();
|
||||
const auto left = full.height() - inner;
|
||||
if (left >= 2 * size) {
|
||||
_progress->widget.setGeometry(
|
||||
full.x() + (full.width() - size) / 2,
|
||||
full.y() + inner + (left - size) / 2,
|
||||
size,
|
||||
size);
|
||||
} else {
|
||||
_progress->widget.setGeometry(full);
|
||||
}
|
||||
}, _progress->geometryLifetime);
|
||||
} else if (_weakEditInformation) {
|
||||
_weakEditInformation->geometryValue(
|
||||
) | rpl::on_next([=] {
|
||||
_progress->widget.setGeometry(_widget->innerGeometry());
|
||||
}, _progress->geometryLifetime);
|
||||
} else if (_weakEditCard) {
|
||||
_weakEditCard->geometryValue(
|
||||
) | rpl::on_next([=] {
|
||||
_progress->widget.setGeometry(_widget->innerGeometry());
|
||||
}, _progress->geometryLifetime);
|
||||
}
|
||||
_progress->widget.show();
|
||||
_progress->widget.raise();
|
||||
if (_progress->shown) {
|
||||
_progress->widget.setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::showForm(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options) {
|
||||
if (invoice
|
||||
&& method.savedMethods.empty()
|
||||
&& !method.native.supported) {
|
||||
const auto available = Webview::Availability();
|
||||
if (available.error != Webview::Available::Error::None) {
|
||||
showWebviewError(
|
||||
tr::lng_payments_webview_no_use(tr::now),
|
||||
available);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_testMode = invoice.isTest;
|
||||
setTitle(invoice.receipt
|
||||
? tr::lng_payments_receipt_title()
|
||||
: tr::lng_payments_checkout_title());
|
||||
auto form = base::make_unique_q<FormSummary>(
|
||||
_widget.get(),
|
||||
invoice,
|
||||
current,
|
||||
method,
|
||||
options,
|
||||
_delegate,
|
||||
_formScrollTop.current());
|
||||
_weakFormSummary = form.get();
|
||||
_widget->showInner(std::move(form));
|
||||
_widget->setBackAllowed(false);
|
||||
_formScrollTop = _weakFormSummary->scrollTopValue();
|
||||
setupProgressGeometry();
|
||||
}
|
||||
|
||||
void Panel::updateFormThumbnail(const QImage &thumbnail) {
|
||||
if (_weakFormSummary) {
|
||||
_weakFormSummary->updateThumbnail(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::showEditInformation(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field) {
|
||||
setTitle(tr::lng_payments_shipping_address_title());
|
||||
auto edit = base::make_unique_q<EditInformation>(
|
||||
_widget.get(),
|
||||
invoice,
|
||||
current,
|
||||
field,
|
||||
_delegate);
|
||||
_weakEditInformation = edit.get();
|
||||
_widget->showInner(std::move(edit));
|
||||
_widget->setBackAllowed(true);
|
||||
_weakEditInformation->setFocusFast(field);
|
||||
setupProgressGeometry();
|
||||
}
|
||||
|
||||
void Panel::showInformationError(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field) {
|
||||
if (_weakEditInformation) {
|
||||
_weakEditInformation->showError(field);
|
||||
} else {
|
||||
showEditInformation(invoice, current, field);
|
||||
if (_weakEditInformation
|
||||
&& field == InformationField::ShippingCountry) {
|
||||
_weakEditInformation->showError(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::chooseShippingOption(const ShippingOptions &options) {
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
const auto i = ranges::find(
|
||||
options.list,
|
||||
options.selectedId,
|
||||
&ShippingOption::id);
|
||||
const auto index = (i != end(options.list))
|
||||
? int(i - begin(options.list))
|
||||
: -1;
|
||||
const auto group = std::make_shared<RadiobuttonGroup>(index);
|
||||
|
||||
const auto layout = box->verticalLayout();
|
||||
auto counter = 0;
|
||||
for (const auto &option : options.list) {
|
||||
const auto index = counter++;
|
||||
const auto button = layout->add(
|
||||
object_ptr<Radiobutton>(
|
||||
layout,
|
||||
group,
|
||||
index,
|
||||
QString(),
|
||||
st::defaultBoxCheckbox,
|
||||
st::defaultRadio),
|
||||
st::paymentsShippingMargin);
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
layout.get(),
|
||||
option.title,
|
||||
st::paymentsShippingLabel);
|
||||
const auto total = ranges::accumulate(
|
||||
option.prices,
|
||||
int64(0),
|
||||
std::plus<>(),
|
||||
&LabeledPrice::price);
|
||||
const auto price = CreateChild<FlatLabel>(
|
||||
layout.get(),
|
||||
FillAmountAndCurrency(total, options.currency),
|
||||
st::paymentsShippingPrice);
|
||||
const auto area = CreateChild<AbstractButton>(layout.get());
|
||||
area->setClickedCallback([=] { group->setValue(index); });
|
||||
button->geometryValue(
|
||||
) | rpl::on_next([=](QRect geometry) {
|
||||
label->move(
|
||||
geometry.topLeft() + st::paymentsShippingLabelPosition);
|
||||
price->move(
|
||||
geometry.topLeft() + st::paymentsShippingPricePosition);
|
||||
const auto right = geometry.x()
|
||||
+ st::paymentsShippingLabelPosition.x();
|
||||
area->setGeometry(
|
||||
right,
|
||||
geometry.y(),
|
||||
std::max(
|
||||
label->x() + label->width() - right,
|
||||
price->x() + price->width() - right),
|
||||
price->y() + price->height() - geometry.y());
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
box->setTitle(tr::lng_payments_shipping_method());
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
group->setChangedCallback([=](int index) {
|
||||
if (index >= 0) {
|
||||
_delegate->panelChangeShippingOption(
|
||||
options.list[index].id);
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::chooseTips(const Invoice &invoice) {
|
||||
const auto max = invoice.tipsMax;
|
||||
const auto now = invoice.tipsSelected;
|
||||
const auto currency = invoice.currency;
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
box->setTitle(tr::lng_payments_tips_box_title());
|
||||
const auto row = box->lifetime().make_state<Field>(
|
||||
box,
|
||||
FieldConfig{
|
||||
.type = FieldType::Money,
|
||||
.value = QString::number(now),
|
||||
.currency = currency,
|
||||
});
|
||||
box->setFocusCallback([=] {
|
||||
row->setFocusFast();
|
||||
});
|
||||
box->addRow(row->ownedWidget());
|
||||
const auto errorWrap = box->addRow(
|
||||
object_ptr<FadeWrap<FlatLabel>>(
|
||||
box,
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_payments_tips_max(
|
||||
lt_amount,
|
||||
rpl::single(FillAmountAndCurrency(max, currency))),
|
||||
st::paymentTipsErrorLabel)),
|
||||
st::paymentTipsErrorPadding);
|
||||
errorWrap->hide(anim::type::instant);
|
||||
const auto submit = [=] {
|
||||
const auto value = row->value().toLongLong();
|
||||
if (value > max) {
|
||||
row->showError();
|
||||
errorWrap->show(anim::type::normal);
|
||||
} else {
|
||||
_delegate->panelChangeTips(value);
|
||||
box->closeBox();
|
||||
}
|
||||
};
|
||||
row->submitted(
|
||||
) | rpl::on_next(submit, box->lifetime());
|
||||
box->addButton(tr::lng_settings_save(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
|
||||
setTitle(tr::lng_payments_card_title());
|
||||
if (method.native.supported) {
|
||||
showEditCard(method.native, CardField::Number);
|
||||
} else {
|
||||
showEditCardByUrl(
|
||||
method.url,
|
||||
method.provider,
|
||||
method.canSaveInformation);
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::showEditCardByUrl(
|
||||
const QString &url,
|
||||
const QString &provider,
|
||||
bool canSaveInformation) {
|
||||
auto bottomText = canSaveInformation
|
||||
? rpl::producer<QString>()
|
||||
: tr::lng_payments_processed_by(lt_provider, rpl::single(provider));
|
||||
if (!showWebview(url, true, std::move(bottomText))) {
|
||||
const auto available = Webview::Availability();
|
||||
if (available.error != Webview::Available::Error::None) {
|
||||
showWebviewError(
|
||||
tr::lng_payments_webview_no_use(tr::now),
|
||||
available);
|
||||
} else {
|
||||
showCriticalError({ "Error: Could not initialize WebView." });
|
||||
}
|
||||
_widget->setBackAllowed(true);
|
||||
} else if (canSaveInformation) {
|
||||
const auto &padding = st::paymentsPanelPadding;
|
||||
_saveWebviewInformation = CreateChild<Checkbox>(
|
||||
_webviewBottom.get(),
|
||||
tr::lng_payments_save_information(tr::now),
|
||||
false);
|
||||
const auto height = padding.top()
|
||||
+ _saveWebviewInformation->heightNoMargins()
|
||||
+ padding.bottom();
|
||||
_saveWebviewInformation->moveToLeft(padding.right(), padding.top());
|
||||
_saveWebviewInformation->show();
|
||||
_webviewBottom->resize(_webviewBottom->width(), height);
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::showAdditionalMethod(
|
||||
const PaymentMethodAdditional &method,
|
||||
const QString &provider,
|
||||
bool canSaveInformation) {
|
||||
setTitle(rpl::single(method.title));
|
||||
showEditCardByUrl(method.url, provider, canSaveInformation);
|
||||
}
|
||||
|
||||
void Panel::showWebviewProgress() {
|
||||
if (_webviewProgress && _progress && _progress->shown) {
|
||||
return;
|
||||
}
|
||||
_webviewProgress = true;
|
||||
toggleProgress(true);
|
||||
}
|
||||
|
||||
void Panel::hideWebviewProgress() {
|
||||
if (!_webviewProgress) {
|
||||
return;
|
||||
}
|
||||
_webviewProgress = false;
|
||||
toggleProgress(false);
|
||||
}
|
||||
|
||||
bool Panel::showWebview(
|
||||
const QString &url,
|
||||
bool allowBack,
|
||||
rpl::producer<QString> bottomText) {
|
||||
const auto params = _delegate->panelWebviewThemeParams();
|
||||
if (!_webview && !createWebview(params)) {
|
||||
return false;
|
||||
}
|
||||
showWebviewProgress();
|
||||
_widget->hideLayer(anim::type::instant);
|
||||
updateThemeParams(params);
|
||||
_webview->window.navigate(url);
|
||||
_widget->setBackAllowed(allowBack);
|
||||
if (bottomText) {
|
||||
const auto &padding = st::paymentsPanelPadding;
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
_webviewBottom.get(),
|
||||
std::move(bottomText),
|
||||
st::paymentsWebviewBottom);
|
||||
const auto height = padding.top()
|
||||
+ label->heightNoMargins()
|
||||
+ padding.bottom();
|
||||
rpl::combine(
|
||||
_webviewBottom->widthValue(),
|
||||
label->widthValue()
|
||||
) | rpl::on_next([=](int outerWidth, int width) {
|
||||
label->move((outerWidth - width) / 2, padding.top());
|
||||
}, label->lifetime());
|
||||
label->show();
|
||||
_webviewBottom->resize(_webviewBottom->width(), height);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
|
||||
auto outer = base::make_unique_q<RpWidget>(_widget.get());
|
||||
const auto container = outer.get();
|
||||
_widget->showInner(std::move(outer));
|
||||
const auto webviewParent = QPointer<RpWidget>(container);
|
||||
|
||||
_webviewBottom = std::make_unique<RpWidget>(_widget.get());
|
||||
const auto bottom = _webviewBottom.get();
|
||||
bottom->show();
|
||||
|
||||
rpl::combine(
|
||||
container->geometryValue() | rpl::map([=] {
|
||||
return _widget->innerGeometry();
|
||||
}),
|
||||
bottom->heightValue()
|
||||
) | rpl::on_next([=](QRect inner, int height) {
|
||||
bottom->move(inner.x(), inner.y() + inner.height() - height);
|
||||
bottom->resizeToWidth(inner.width());
|
||||
_footerHeight = bottom->height();
|
||||
}, bottom->lifetime());
|
||||
container->show();
|
||||
|
||||
_webview = std::make_unique<WebviewWithLifetime>(
|
||||
container,
|
||||
Webview::WindowConfig{
|
||||
.opaqueBg = params.bodyBg,
|
||||
.storageId = _delegate->panelWebviewStorageId(),
|
||||
});
|
||||
|
||||
const auto raw = &_webview->window;
|
||||
QObject::connect(container, &QObject::destroyed, [=] {
|
||||
if (_webview && &_webview->window == raw) {
|
||||
base::take(_webview);
|
||||
if (_webviewProgress) {
|
||||
hideWebviewProgress();
|
||||
if (_progress && !_progress->shown) {
|
||||
_progress = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_webviewBottom.get() == bottom) {
|
||||
_webviewBottom = nullptr;
|
||||
}
|
||||
});
|
||||
if (!raw->widget()) {
|
||||
return false;
|
||||
}
|
||||
QObject::connect(raw->widget(), &QObject::destroyed, [=] {
|
||||
const auto parent = webviewParent.data();
|
||||
if (!_webview
|
||||
|| &_webview->window != raw
|
||||
|| !parent
|
||||
|| _widget->inner() != parent) {
|
||||
// If we destroyed _webview ourselves,
|
||||
// or if we changed _widget->inner ourselves,
|
||||
// we don't show any message, nothing crashed.
|
||||
return;
|
||||
}
|
||||
crl::on_main(this, [=] {
|
||||
showCriticalError({ "Error: WebView has crashed." });
|
||||
});
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
container->geometryValue(),
|
||||
_footerHeight.value()
|
||||
) | rpl::on_next([=](QRect geometry, int footer) {
|
||||
if (const auto view = raw->widget()) {
|
||||
view->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));
|
||||
}
|
||||
}, _webview->lifetime);
|
||||
|
||||
raw->setMessageHandler([=](const QJsonDocument &message) {
|
||||
const auto save = _saveWebviewInformation
|
||||
&& _saveWebviewInformation->checked();
|
||||
_delegate->panelWebviewMessage(message, save);
|
||||
});
|
||||
|
||||
raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
|
||||
if (!_delegate->panelWebviewNavigationAttempt(uri)) {
|
||||
return false;
|
||||
} else if (newWindow) {
|
||||
return false;
|
||||
}
|
||||
showWebviewProgress();
|
||||
return true;
|
||||
});
|
||||
raw->setNavigationDoneHandler([=](bool success) {
|
||||
hideWebviewProgress();
|
||||
});
|
||||
|
||||
raw->init(R"(
|
||||
window.TelegramWebviewProxy = {
|
||||
postEvent: function(eventType, eventData) {
|
||||
if (window.external && window.external.invoke) {
|
||||
window.external.invoke(JSON.stringify([eventType, eventData]));
|
||||
}
|
||||
}
|
||||
};)");
|
||||
|
||||
if (!_webview) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setupProgressGeometry();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Panel::choosePaymentMethod(const PaymentMethodDetails &method) {
|
||||
if (method.savedMethods.empty() && method.additionalMethods.empty()) {
|
||||
showEditPaymentMethod(method);
|
||||
return;
|
||||
}
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
const auto save = [=](int option) {
|
||||
const auto saved = int(method.savedMethods.size());
|
||||
if (!option) {
|
||||
showEditPaymentMethod(method);
|
||||
} else if (option > saved) {
|
||||
const auto index = option - saved - 1;
|
||||
Assert(index < method.additionalMethods.size());
|
||||
showAdditionalMethod(
|
||||
method.additionalMethods[index],
|
||||
method.provider,
|
||||
method.canSaveInformation);
|
||||
} else {
|
||||
const auto index = option - 1;
|
||||
_savedMethodChosen.fire_copy(method.savedMethods[index].id);
|
||||
}
|
||||
};
|
||||
auto options = std::vector{
|
||||
tr::lng_payments_new_card(tr::now),
|
||||
};
|
||||
for (const auto &saved : method.savedMethods) {
|
||||
options.push_back(saved.title);
|
||||
}
|
||||
for (const auto &additional : method.additionalMethods) {
|
||||
options.push_back(additional.title);
|
||||
}
|
||||
SingleChoiceBox(box, {
|
||||
.title = tr::lng_payments_payment_method(),
|
||||
.options = std::move(options),
|
||||
.initialSelection = (method.savedMethods.empty()
|
||||
? -1
|
||||
: (method.savedMethodIndex + 1)),
|
||||
.callback = save,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::askSetPassword() {
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_payments_need_password(),
|
||||
st::boxLabel),
|
||||
st::boxPadding);
|
||||
box->addButton(tr::lng_continue(), [=] {
|
||||
_delegate->panelSetPassword();
|
||||
box->closeBox();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::showCloseConfirm() {
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_payments_sure_close(),
|
||||
st::boxLabel),
|
||||
st::boxPadding);
|
||||
box->addButton(tr::lng_close(), [=] {
|
||||
_delegate->panelCloseSure();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::showWarning(const QString &bot, const QString &provider) {
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
box->setTitle(tr::lng_payments_warning_title());
|
||||
box->addRow(object_ptr<FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_payments_warning_body(
|
||||
lt_bot1,
|
||||
rpl::single(bot),
|
||||
lt_provider,
|
||||
rpl::single(provider),
|
||||
lt_bot2,
|
||||
rpl::single(bot),
|
||||
lt_bot3,
|
||||
rpl::single(bot)),
|
||||
st::boxLabel));
|
||||
box->addButton(tr::lng_continue(), [=] {
|
||||
_delegate->panelTrustAndSubmit();
|
||||
box->closeBox();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::requestTermsAcceptance(
|
||||
const QString &username,
|
||||
const QString &url,
|
||||
bool recurring) {
|
||||
showBox(Box([=](not_null<GenericBox*> box) {
|
||||
box->setTitle(tr::lng_payments_terms_title());
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
(recurring
|
||||
? tr::lng_payments_terms_text
|
||||
: tr::lng_payments_terms_text_once)(
|
||||
lt_bot,
|
||||
rpl::single(tr::bold('@' + username)),
|
||||
tr::marked),
|
||||
st::boxLabel));
|
||||
const auto update = std::make_shared<Fn<void()>>();
|
||||
auto checkView = std::make_unique<Ui::CheckView>(
|
||||
st::defaultCheck,
|
||||
false,
|
||||
[=] { if (*update) { (*update)(); } });
|
||||
const auto check = checkView.get();
|
||||
const auto row = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box.get(),
|
||||
tr::lng_payments_terms_agree(
|
||||
lt_link,
|
||||
rpl::single(tr::link(
|
||||
tr::lng_payments_terms_link(tr::now),
|
||||
url)),
|
||||
tr::marked),
|
||||
st::defaultBoxCheckbox,
|
||||
std::move(checkView)),
|
||||
{
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.left(),
|
||||
st::boxRowPadding.right(),
|
||||
st::defaultBoxCheckbox.margin.bottom(),
|
||||
});
|
||||
row->setAllowTextLines(5);
|
||||
row->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &link,
|
||||
Qt::MouseButton button) {
|
||||
ActivateClickHandler(_widget.get(), link, ClickContext{
|
||||
.button = button,
|
||||
.other = _delegate->panelClickHandlerContext(),
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
(*update) = [=] { row->update(); };
|
||||
|
||||
const auto showError = Ui::CheckView::PrepareNonToggledError(
|
||||
check,
|
||||
box->lifetime());
|
||||
|
||||
box->addButton(tr::lng_payments_terms_accept(), [=] {
|
||||
if (check->checked()) {
|
||||
_delegate->panelAcceptTermsAndSubmit();
|
||||
box->closeBox();
|
||||
} else {
|
||||
showError();
|
||||
}
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::showEditCard(
|
||||
const NativeMethodDetails &native,
|
||||
CardField field) {
|
||||
Expects(native.supported);
|
||||
|
||||
auto edit = base::make_unique_q<EditCard>(
|
||||
_widget.get(),
|
||||
native,
|
||||
field,
|
||||
_delegate);
|
||||
_weakEditCard = edit.get();
|
||||
_widget->showInner(std::move(edit));
|
||||
_widget->setBackAllowed(true);
|
||||
_weakEditCard->setFocusFast(field);
|
||||
setupProgressGeometry();
|
||||
}
|
||||
|
||||
void Panel::showCardError(
|
||||
const NativeMethodDetails &native,
|
||||
CardField field) {
|
||||
if (_weakEditCard) {
|
||||
_weakEditCard->showError(field);
|
||||
} else {
|
||||
// We cancelled card edit already.
|
||||
//showEditCard(native, field);
|
||||
//if (_weakEditCard
|
||||
// && field == CardField::AddressCountry) {
|
||||
// _weakEditCard->showError(field);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::setTitle(rpl::producer<QString> title) {
|
||||
using namespace rpl::mappers;
|
||||
if (_testMode) {
|
||||
_widget->setTitle(std::move(title) | rpl::map(_1 + " (Test)"));
|
||||
} else {
|
||||
_widget->setTitle(std::move(title));
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<> Panel::backRequests() const {
|
||||
return _widget->backRequests();
|
||||
}
|
||||
|
||||
rpl::producer<QString> Panel::savedMethodChosen() const {
|
||||
return _savedMethodChosen.events();
|
||||
}
|
||||
|
||||
void Panel::showBox(object_ptr<BoxContent> box) {
|
||||
if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
|
||||
const auto hideNow = !widget->isHidden();
|
||||
if (hideNow || _webview->lastHidingBox) {
|
||||
const auto raw = _webview->lastHidingBox = box.data();
|
||||
box->boxClosing(
|
||||
) | rpl::on_next([=] {
|
||||
const auto widget = _webview
|
||||
? _webview->window.widget()
|
||||
: nullptr;
|
||||
if (widget
|
||||
&& widget->isHidden()
|
||||
&& _webview->lastHidingBox == raw) {
|
||||
widget->show();
|
||||
}
|
||||
}, _webview->lifetime);
|
||||
if (hideNow) {
|
||||
widget->hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
_widget->showBox(
|
||||
std::move(box),
|
||||
LayerOption::KeepOther,
|
||||
anim::type::normal);
|
||||
}
|
||||
|
||||
void Panel::showToast(TextWithEntities &&text) {
|
||||
_widget->showToast(std::move(text));
|
||||
}
|
||||
|
||||
void Panel::showCriticalError(const TextWithEntities &text) {
|
||||
_progress = nullptr;
|
||||
_webviewProgress = false;
|
||||
if (!_weakFormSummary || !_weakFormSummary->showCriticalError(text)) {
|
||||
auto wrap = base::make_unique_q<RpWidget>(_widget.get());
|
||||
const auto raw = wrap.get();
|
||||
|
||||
const auto error = CreateChild<PaddingWrap<FlatLabel>>(
|
||||
raw,
|
||||
object_ptr<FlatLabel>(
|
||||
raw,
|
||||
rpl::single(text),
|
||||
st::paymentsCriticalError),
|
||||
st::paymentsCriticalErrorPadding);
|
||||
error->entity()->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton) {
|
||||
const auto entity = handler->getTextEntity();
|
||||
if (entity.type != EntityType::CustomUrl) {
|
||||
return true;
|
||||
}
|
||||
_delegate->panelOpenUrl(entity.data);
|
||||
return false;
|
||||
});
|
||||
|
||||
raw->widthValue() | rpl::on_next([=](int width) {
|
||||
error->resizeToWidth(width);
|
||||
raw->resize(width, error->height());
|
||||
}, raw->lifetime());
|
||||
|
||||
_widget->showInner(std::move(wrap));
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Show> Panel::uiShow() {
|
||||
return _widget->uiShow();
|
||||
}
|
||||
|
||||
void Panel::showWebviewError(
|
||||
const QString &text,
|
||||
const Webview::Available &information) {
|
||||
showCriticalError(TextWithEntities{ text }.append(
|
||||
"\n\n"
|
||||
).append(BotWebView::ErrorText(information)));
|
||||
}
|
||||
|
||||
void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) {
|
||||
if (!_webview || !_webview->window.widget()) {
|
||||
return;
|
||||
}
|
||||
_webview->window.updateTheme(
|
||||
params.bodyBg,
|
||||
params.scrollBg,
|
||||
params.scrollBgOver,
|
||||
params.scrollBarBg,
|
||||
params.scrollBarBgOver);
|
||||
_webview->window.eval(R"(
|
||||
if (window.TelegramGameProxy) {
|
||||
window.TelegramGameProxy.receiveEvent(
|
||||
"theme_changed",
|
||||
{ "theme_params": )" + params.json + R"( });
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
rpl::lifetime &Panel::lifetime() {
|
||||
return _widget->lifetime();
|
||||
}
|
||||
|
||||
} // namespace Payments::Ui
|
||||
138
Telegram/SourceFiles/payments/ui/payments_panel.h
Normal file
138
Telegram/SourceFiles/payments/ui/payments_panel.h
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class SeparatePanel;
|
||||
class BoxContent;
|
||||
class Checkbox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Webview {
|
||||
struct Available;
|
||||
struct ThemeParams;
|
||||
} // namespace Webview
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class PanelDelegate;
|
||||
struct Invoice;
|
||||
struct RequestedInformation;
|
||||
struct ShippingOptions;
|
||||
enum class InformationField;
|
||||
enum class CardField;
|
||||
class FormSummary;
|
||||
class EditInformation;
|
||||
class EditCard;
|
||||
struct PaymentMethodDetails;
|
||||
struct PaymentMethodAdditional;
|
||||
struct NativeMethodDetails;
|
||||
|
||||
class Panel final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Panel(not_null<PanelDelegate*> delegate);
|
||||
~Panel();
|
||||
|
||||
void requestActivate();
|
||||
void toggleProgress(bool shown);
|
||||
|
||||
void showForm(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
const PaymentMethodDetails &method,
|
||||
const ShippingOptions &options);
|
||||
void updateFormThumbnail(const QImage &thumbnail);
|
||||
void showEditInformation(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field);
|
||||
void showInformationError(
|
||||
const Invoice &invoice,
|
||||
const RequestedInformation ¤t,
|
||||
InformationField field);
|
||||
void showEditPaymentMethod(const PaymentMethodDetails &method);
|
||||
void showAdditionalMethod(
|
||||
const PaymentMethodAdditional &method,
|
||||
const QString &provider,
|
||||
bool canSaveInformation);
|
||||
void showEditCard(const NativeMethodDetails &native, CardField field);
|
||||
void showEditCardByUrl(
|
||||
const QString &url,
|
||||
const QString &provider,
|
||||
bool canSaveInformation);
|
||||
void showCardError(const NativeMethodDetails &native, CardField field);
|
||||
void chooseShippingOption(const ShippingOptions &options);
|
||||
void chooseTips(const Invoice &invoice);
|
||||
void choosePaymentMethod(const PaymentMethodDetails &method);
|
||||
void askSetPassword();
|
||||
void showCloseConfirm();
|
||||
void showWarning(const QString &bot, const QString &provider);
|
||||
void requestTermsAcceptance(
|
||||
const QString &username,
|
||||
const QString &url,
|
||||
bool recurring);
|
||||
|
||||
bool showWebview(
|
||||
const QString &url,
|
||||
bool allowBack,
|
||||
rpl::producer<QString> bottomText);
|
||||
void updateThemeParams(const Webview::ThemeParams ¶ms);
|
||||
|
||||
[[nodiscard]] rpl::producer<> backRequests() const;
|
||||
[[nodiscard]] rpl::producer<QString> savedMethodChosen() const;
|
||||
|
||||
void showBox(object_ptr<Ui::BoxContent> box);
|
||||
void showToast(TextWithEntities &&text);
|
||||
void showCriticalError(const TextWithEntities &text);
|
||||
[[nodiscard]] std::shared_ptr<Show> uiShow();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Progress;
|
||||
struct WebviewWithLifetime;
|
||||
|
||||
bool createWebview(const Webview::ThemeParams ¶ms);
|
||||
void showWebviewProgress();
|
||||
void hideWebviewProgress();
|
||||
void showWebviewError(
|
||||
const QString &text,
|
||||
const Webview::Available &information);
|
||||
void setTitle(rpl::producer<QString> title);
|
||||
|
||||
[[nodiscard]] bool progressWithBackground() const;
|
||||
[[nodiscard]] QRect progressRect() const;
|
||||
void setupProgressGeometry();
|
||||
void updateFooterHeight();
|
||||
|
||||
const not_null<PanelDelegate*> _delegate;
|
||||
std::unique_ptr<SeparatePanel> _widget;
|
||||
std::unique_ptr<WebviewWithLifetime> _webview;
|
||||
std::unique_ptr<RpWidget> _webviewBottom;
|
||||
rpl::variable<int> _footerHeight;
|
||||
std::unique_ptr<Progress> _progress;
|
||||
QPointer<Checkbox> _saveWebviewInformation;
|
||||
QPointer<FormSummary> _weakFormSummary;
|
||||
rpl::variable<int> _formScrollTop;
|
||||
QPointer<EditInformation> _weakEditInformation;
|
||||
QPointer<EditCard> _weakEditCard;
|
||||
rpl::event_stream<QString> _savedMethodChosen;
|
||||
bool _themeUpdateScheduled = false;
|
||||
bool _webviewProgress = false;
|
||||
bool _testMode = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
204
Telegram/SourceFiles/payments/ui/payments_panel_data.h
Normal file
204
Telegram/SourceFiles/payments/ui/payments_panel_data.h
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_entity.h"
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
struct LabeledPrice {
|
||||
QString label;
|
||||
int64 price = 0;
|
||||
};
|
||||
|
||||
struct Cover {
|
||||
QString title;
|
||||
TextWithEntities description;
|
||||
QString seller;
|
||||
QImage thumbnail;
|
||||
};
|
||||
|
||||
struct Receipt {
|
||||
TimeId date = 0;
|
||||
int64 totalAmount = 0;
|
||||
QString currency;
|
||||
bool paid = false;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return !paid;
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct Invoice {
|
||||
Cover cover;
|
||||
|
||||
std::vector<LabeledPrice> prices;
|
||||
std::vector<int64> suggestedTips;
|
||||
int64 tipsMax = 0;
|
||||
int64 tipsSelected = 0;
|
||||
QString currency;
|
||||
Receipt receipt;
|
||||
|
||||
bool isNameRequested = false;
|
||||
bool isPhoneRequested = false;
|
||||
bool isEmailRequested = false;
|
||||
bool isShippingAddressRequested = false;
|
||||
bool isRecurring = false;
|
||||
bool isFlexible = false;
|
||||
bool isTest = false;
|
||||
|
||||
QString provider;
|
||||
QString termsUrl;
|
||||
bool phoneSentToProvider = false;
|
||||
bool emailSentToProvider = false;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
return !currency.isEmpty() && (!prices.empty() || tipsMax);
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
};
|
||||
|
||||
struct ShippingOption {
|
||||
QString id;
|
||||
QString title;
|
||||
std::vector<LabeledPrice> prices;
|
||||
};
|
||||
|
||||
struct ShippingOptions {
|
||||
QString currency;
|
||||
std::vector<ShippingOption> list;
|
||||
QString selectedId;
|
||||
};
|
||||
|
||||
struct Address {
|
||||
QString address1;
|
||||
QString address2;
|
||||
QString city;
|
||||
QString state;
|
||||
QString countryIso2;
|
||||
QString postcode;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
return !address1.isEmpty()
|
||||
&& !city.isEmpty()
|
||||
&& !countryIso2.isEmpty();
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
|
||||
inline bool operator==(const Address &other) const {
|
||||
return (address1 == other.address1)
|
||||
&& (address2 == other.address2)
|
||||
&& (city == other.city)
|
||||
&& (state == other.state)
|
||||
&& (countryIso2 == other.countryIso2)
|
||||
&& (postcode == other.postcode);
|
||||
}
|
||||
inline bool operator!=(const Address &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
struct RequestedInformation {
|
||||
QString defaultPhone;
|
||||
QString defaultCountry;
|
||||
bool save = true;
|
||||
|
||||
QString name;
|
||||
QString phone;
|
||||
QString email;
|
||||
Address shippingAddress;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return name.isEmpty()
|
||||
&& phone.isEmpty()
|
||||
&& email.isEmpty()
|
||||
&& !shippingAddress;
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
|
||||
inline bool operator==(const RequestedInformation &other) const {
|
||||
return (name == other.name)
|
||||
&& (phone == other.phone)
|
||||
&& (email == other.email)
|
||||
&& (shippingAddress == other.shippingAddress);
|
||||
}
|
||||
inline bool operator!=(const RequestedInformation &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
enum class InformationField {
|
||||
ShippingStreet,
|
||||
ShippingCity,
|
||||
ShippingState,
|
||||
ShippingCountry,
|
||||
ShippingPostcode,
|
||||
Name,
|
||||
Email,
|
||||
Phone,
|
||||
};
|
||||
|
||||
struct NativeMethodDetails {
|
||||
QString defaultCountry;
|
||||
|
||||
bool supported = false;
|
||||
bool needCountry = false;
|
||||
bool needZip = false;
|
||||
bool needCardholderName = false;
|
||||
bool canSaveInformation = false;
|
||||
};
|
||||
|
||||
struct PaymentMethodAdditional {
|
||||
QString title;
|
||||
QString url;
|
||||
};
|
||||
|
||||
struct PaymentMethodSaved {
|
||||
QString id;
|
||||
QString title;
|
||||
};
|
||||
|
||||
struct PaymentMethodDetails {
|
||||
NativeMethodDetails native;
|
||||
std::vector<PaymentMethodSaved> savedMethods;
|
||||
std::vector<PaymentMethodAdditional> additionalMethods;
|
||||
QString url;
|
||||
QString provider;
|
||||
int savedMethodIndex = 0;
|
||||
bool canSaveInformation = false;
|
||||
};
|
||||
|
||||
enum class CardField {
|
||||
Number,
|
||||
Cvc,
|
||||
ExpireDate,
|
||||
Name,
|
||||
AddressCountry,
|
||||
AddressZip,
|
||||
};
|
||||
|
||||
struct UncheckedCardDetails {
|
||||
QString number;
|
||||
QString cvc;
|
||||
uint32 expireYear = 0;
|
||||
uint32 expireMonth = 0;
|
||||
QString cardholderName;
|
||||
QString addressCountry;
|
||||
QString addressZip;
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
69
Telegram/SourceFiles/payments/ui/payments_panel_delegate.h
Normal file
69
Telegram/SourceFiles/payments/ui/payments_panel_delegate.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
class QJsonDocument;
|
||||
class QString;
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Webview {
|
||||
struct ThemeParams;
|
||||
struct StorageId;
|
||||
} // namespace Webview
|
||||
|
||||
namespace Payments::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct RequestedInformation;
|
||||
struct UncheckedCardDetails;
|
||||
|
||||
class PanelDelegate {
|
||||
public:
|
||||
virtual void panelRequestClose() = 0;
|
||||
virtual void panelCloseSure() = 0;
|
||||
virtual void panelSubmit() = 0;
|
||||
virtual void panelTrustAndSubmit() = 0;
|
||||
virtual void panelAcceptTermsAndSubmit() = 0;
|
||||
virtual void panelWebviewMessage(
|
||||
const QJsonDocument &message,
|
||||
bool saveInformation) = 0;
|
||||
virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0;
|
||||
virtual void panelSetPassword() = 0;
|
||||
virtual void panelOpenUrl(const QString &url) = 0;
|
||||
|
||||
virtual void panelCancelEdit() = 0;
|
||||
virtual void panelEditPaymentMethod() = 0;
|
||||
virtual void panelEditShippingInformation() = 0;
|
||||
virtual void panelEditName() = 0;
|
||||
virtual void panelEditEmail() = 0;
|
||||
virtual void panelEditPhone() = 0;
|
||||
virtual void panelChooseShippingOption() = 0;
|
||||
virtual void panelChangeShippingOption(const QString &id) = 0;
|
||||
virtual void panelChooseTips() = 0;
|
||||
virtual void panelChangeTips(int64 value) = 0;
|
||||
|
||||
virtual void panelValidateInformation(RequestedInformation data) = 0;
|
||||
virtual void panelValidateCard(
|
||||
Ui::UncheckedCardDetails data,
|
||||
bool saveInformation) = 0;
|
||||
virtual void panelShowBox(object_ptr<BoxContent> box) = 0;
|
||||
virtual QVariant panelClickHandlerContext() = 0;
|
||||
|
||||
virtual Webview::StorageId panelWebviewStorageId() = 0;
|
||||
virtual Webview::ThemeParams panelWebviewThemeParams() = 0;
|
||||
|
||||
virtual std::optional<QDate> panelOverrideExpireDateThreshold() = 0;
|
||||
};
|
||||
|
||||
} // namespace Payments::Ui
|
||||
1035
Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp
Normal file
1035
Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
120
Telegram/SourceFiles/payments/ui/payments_reaction_box.h
Normal file
120
Telegram/SourceFiles/payments/ui/payments_reaction_box.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/group/ui/calls_group_stars_coloring.h"
|
||||
|
||||
namespace style {
|
||||
struct RoundCheckbox;
|
||||
struct MediaSlider;
|
||||
} // namespace style
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui::Premium {
|
||||
class BubbleWidget;
|
||||
} // namespace Ui::Premium
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractButton;
|
||||
class BoxContent;
|
||||
class GenericBox;
|
||||
class DynamicImage;
|
||||
class VerticalLayout;
|
||||
|
||||
struct PaidReactionTop {
|
||||
QString name;
|
||||
std::shared_ptr<DynamicImage> photo;
|
||||
uint64 barePeerId = 0;
|
||||
int count = 0;
|
||||
Fn<void()> click;
|
||||
bool my = false;
|
||||
};
|
||||
|
||||
struct PaidReactionBoxArgs {
|
||||
int min = 0;
|
||||
int explicitlyAllowed = 0;
|
||||
int chosen = 0;
|
||||
int max = 0;
|
||||
|
||||
std::vector<PaidReactionTop> top;
|
||||
|
||||
not_null<Main::Session*> session;
|
||||
QString name;
|
||||
Fn<rpl::producer<TextWithEntities>(rpl::producer<int> amount)> submit;
|
||||
std::vector<Calls::Group::Ui::StarsColoring> colorings;
|
||||
rpl::producer<CreditsAmount> balanceValue;
|
||||
Fn<void(int, uint64)> send;
|
||||
bool videoStreamChoosing = false;
|
||||
bool videoStreamSending = false;
|
||||
bool videoStreamAdmin = false;
|
||||
bool dark = false;
|
||||
};
|
||||
|
||||
void PaidReactionsBox(
|
||||
not_null<GenericBox*> box,
|
||||
PaidReactionBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(
|
||||
PaidReactionBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] int MaxTopPaidDonorsShown();
|
||||
|
||||
[[nodiscard]] QImage GenerateSmallBadgeImage(
|
||||
QString text,
|
||||
const style::icon &icon,
|
||||
QColor bg,
|
||||
QColor fg,
|
||||
const style::RoundCheckbox *borderSt = nullptr);
|
||||
|
||||
struct StarSelectDiscreter {
|
||||
Fn<int(float64)> ratioToValue;
|
||||
Fn<float64(int)> valueToRatio;
|
||||
};
|
||||
|
||||
[[nodiscard]] StarSelectDiscreter StarSelectDiscreterForMax(int max);
|
||||
|
||||
void PaidReactionSlider(
|
||||
not_null<VerticalLayout*> container,
|
||||
const style::MediaSlider &st,
|
||||
int min,
|
||||
int explicitlyAllowed,
|
||||
rpl::producer<int> current,
|
||||
int max,
|
||||
Fn<void(int)> changed,
|
||||
Fn<QColor(int)> activeFgOverride = nullptr);
|
||||
|
||||
void AddStarSelectBalance(
|
||||
not_null<GenericBox*> box,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<CreditsAmount> balanceValue,
|
||||
bool dark = false);
|
||||
|
||||
not_null<Premium::BubbleWidget*> AddStarSelectBubble(
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<> showFinishes,
|
||||
rpl::producer<int> value,
|
||||
int max,
|
||||
Fn<QColor(int)> activeFgOverride = nullptr);
|
||||
|
||||
struct StarSelectInfoBlock {
|
||||
rpl::producer<TextWithEntities> title;
|
||||
rpl::producer<QString> subtext;
|
||||
Fn<void()> click;
|
||||
};
|
||||
[[nodiscard]] object_ptr<RpWidget> MakeStarSelectInfoBlocks(
|
||||
not_null<RpWidget*> parent,
|
||||
std::vector<StarSelectInfoBlock> blocks,
|
||||
Text::MarkedContext context,
|
||||
bool dark = false);
|
||||
|
||||
} // namespace Ui
|
||||
Reference in New Issue
Block a user