Support additional saved payment methods.

This commit is contained in:
John Preston 2022-07-26 17:01:08 +03:00
parent e492a18194
commit f7885da7dd
9 changed files with 155 additions and 77 deletions

View File

@ -2397,7 +2397,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_theme_editor_menu_show" = "Show palette file";
"lng_payments_not_supported" = "Sorry, Telegram Desktop doesn't support payments yet. Please use one of our mobile apps to do this.";
"lng_payments_webview_no_card" = "Unfortunately, you can't add a new card with current system configuration.";
"lng_payments_webview_no_use" = "Unfortunately, you can't use payments with current system configuration.";
"lng_payments_webview_install_edge" = "Please install {link}.";
"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkit2gtk-5.0/webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.";

View File

@ -853,7 +853,7 @@ inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w
upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile;
payments.paymentForm#4cc5563f flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector<PaymentFormMethod> saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector<User> = payments.PaymentForm;
payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector<PaymentFormMethod> saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector<PaymentSavedCredentials> users:Vector<User> = payments.PaymentForm;
payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = payments.ValidatedRequestedInfo;

View File

@ -234,6 +234,11 @@ CheckoutProcess::CheckoutProcess(
handleFormUpdate(update);
}, _lifetime);
_panel->savedMethodChosen(
) | rpl::start_with_next([=](QString id) {
_form->chooseSavedMethod(id);
}, _panel->lifetime());
_panel->backRequests(
) | rpl::start_with_next([=] {
panelCancelEdit();
@ -284,7 +289,7 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) {
if (!_initialSilentValidation) {
showForm();
}
if (_form->paymentMethod().savedCredentials) {
if (!_form->paymentMethod().savedCredentials.empty()) {
_session->api().cloudPassword().reload();
}
}, [&](const ThumbnailUpdated &data) {
@ -532,7 +537,8 @@ void CheckoutProcess::panelSubmit() {
|| invoice.isPhoneRequested)) {
_submitState = SubmitState::Validating;
_form->validateInformation(_form->information());
} else if (!method.newCredentials && !method.savedCredentials) {
} else if (!method.newCredentials
&& method.savedCredentialsIndex >= method.savedCredentials.size()) {
editPaymentMethod();
} else if (invoice.isRecurring && !_form->details().termsAccepted) {
_panel->requestTermsAcceptance(
@ -712,10 +718,13 @@ 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,
_form->paymentMethod().savedCredentials.title);
(index < list.size()) ? list[index].title : QString());
fields.customSubmitButton = tr::lng_payments_password_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result) {

View File

@ -314,10 +314,15 @@ void Form::processForm(const MTPDpayments_paymentForm &data) {
processSavedInformation(data);
});
}
_paymentMethod.savedCredentials.clear();
_paymentMethod.savedCredentialsIndex = 0;
if (const auto credentials = data.vsaved_credentials()) {
credentials->match([&](const auto &data) {
processSavedCredentials(data);
});
_paymentMethod.savedCredentials.reserve(credentials->v.size());
for (const auto &saved : credentials->v) {
saved.match([&](const auto &data) {
addSavedCredentials(data);
});
}
}
if (const auto additional = data.vadditional_methods()) {
processAdditionalPaymentMethods(additional->v);
@ -344,10 +349,11 @@ void Form::processReceipt(const MTPDpayments_paymentReceipt &data) {
_shippingOptions.selectedId = _shippingOptions.list.front().id;
}
}
_paymentMethod.savedCredentials = SavedCredentials{
_paymentMethod.savedCredentials = { {
.id = "(used)",
.title = qs(data.vcredentials_title()),
};
} };
_paymentMethod.savedCredentialsIndex = 0;
fillPaymentMethodInformation();
_updates.fire(FormReady{});
}
@ -467,12 +473,12 @@ void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
};
}
void Form::processSavedCredentials(
void Form::addSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data) {
_paymentMethod.savedCredentials = SavedCredentials{
_paymentMethod.savedCredentials.push_back({
.id = qs(data.vid()),
.title = qs(data.vtitle()),
};
});
refreshPaymentMethodDetails();
}
@ -489,17 +495,33 @@ void Form::processAdditionalPaymentMethods(
}
void Form::refreshPaymentMethodDetails() {
const auto &saved = _paymentMethod.savedCredentials;
const auto &entered = _paymentMethod.newCredentials;
_paymentMethod.ui.title = entered ? entered.title : saved.title;
refreshSavedPaymentMethodDetails();
_paymentMethod.ui.provider = _invoice.provider;
_paymentMethod.ui.ready = entered || saved;
_paymentMethod.ui.native.defaultCountry = defaultCountry();
_paymentMethod.ui.canSaveInformation
= _paymentMethod.ui.native.canSaveInformation
= _details.canSaveCredentials || _details.passwordMissing;
}
void Form::refreshSavedPaymentMethodDetails() {
const auto &list = _paymentMethod.savedCredentials;
const auto index = _paymentMethod.savedCredentialsIndex;
const auto &entered = _paymentMethod.newCredentials;
_paymentMethod.ui.savedMethods.clear();
if (entered) {
_paymentMethod.ui.savedMethods.push_back({ .title = entered.title });
}
for (const auto &item : list) {
_paymentMethod.ui.savedMethods.push_back({
.id = item.id,
.title = item.title,
});
}
_paymentMethod.ui.savedMethodIndex = (index < list.size())
? (index + (entered ? 1 : 0))
: 0;
}
QString Form::defaultPhone() const {
return _session->user()->phone();
}
@ -588,12 +610,16 @@ void Form::fillSmartGlocalNativeMethod(QJsonObject object) {
void Form::submit() {
Expects(_paymentMethod.newCredentials
|| _paymentMethod.savedCredentials);
|| (_paymentMethod.savedCredentialsIndex
< _paymentMethod.savedCredentials.size()));
const auto password = _paymentMethod.newCredentials
? QByteArray()
: _session->validTmpPassword();
if (!_paymentMethod.newCredentials && password.isEmpty()) {
const auto index = _paymentMethod.savedCredentialsIndex;
const auto &list = _paymentMethod.savedCredentials;
const auto password = (index < list.size())
? _session->validTmpPassword()
: QByteArray();
if (index < list.size() && password.isEmpty()) {
_updates.fire(TmpPasswordRequired{});
return;
} else if (!_session->local().isBotTrustedPayment(_details.botId)) {
@ -618,16 +644,16 @@ void Form::submit() {
inputInvoice(),
MTP_string(_requestedInformationId),
MTP_string(_shippingOptions.selectedId),
(_paymentMethod.newCredentials
? MTP_inputPaymentCredentials(
(index < list.size()
? MTP_inputPaymentCredentialsSaved(
MTP_string(list[index].id),
MTP_bytes(password))
: MTP_inputPaymentCredentials(
MTP_flags((_paymentMethod.newCredentials.saveOnServer
&& _details.canSaveCredentials)
? MTPDinputPaymentCredentials::Flag::f_save
: MTPDinputPaymentCredentials::Flag(0)),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))
: MTP_inputPaymentCredentialsSaved(
MTP_string(_paymentMethod.savedCredentials.id),
MTP_bytes(password))),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))),
MTP_long(_invoice.tipsSelected)
)).done([=](const MTPpayments_PaymentResult &result) {
hideProgress();
@ -725,7 +751,9 @@ bool Form::hasChanges() const {
return (information != _savedInformation)
|| (_stripe != nullptr)
|| (_smartglocal != nullptr)
|| !_paymentMethod.newCredentials.empty();
|| (!_paymentMethod.newCredentials.empty()
&& (_paymentMethod.savedCredentialsIndex
>= _paymentMethod.savedCredentials.size()));
}
bool Form::validateInformationLocal(
@ -940,10 +968,30 @@ void Form::setPaymentCredentials(const NewCredentials &credentials) {
Expects(!credentials.empty());
_paymentMethod.newCredentials = credentials;
_paymentMethod.savedCredentialsIndex
= _paymentMethod.savedCredentials.size();
refreshSavedPaymentMethodDetails();
const auto requestNewPassword = credentials.saveOnServer
&& !_details.canSaveCredentials
&& _details.passwordMissing;
refreshPaymentMethodDetails();
_updates.fire(PaymentMethodUpdate{ requestNewPassword });
}
void Form::chooseSavedMethod(const QString &id) {
auto &index = _paymentMethod.savedCredentialsIndex;
const auto &list = _paymentMethod.savedCredentials;
if (id.isEmpty() && _paymentMethod.newCredentials) {
index = list.size();
} else {
const auto i = ranges::find(list, id, &SavedCredentials::id);
index = (i != end(list)) ? (i - begin(list)) : 0;
}
refreshSavedPaymentMethodDetails();
const auto requestNewPassword = (index == list.size())
&& _paymentMethod.newCredentials
&& _paymentMethod.newCredentials.saveOnServer
&& !_details.canSaveCredentials
&& _details.passwordMissing;
_updates.fire(PaymentMethodUpdate{ requestNewPassword });
}

View File

@ -113,7 +113,8 @@ struct NativePaymentMethod {
struct PaymentMethod {
NativePaymentMethod native;
SavedCredentials savedCredentials;
std::vector<SavedCredentials> savedCredentials;
int savedCredentialsIndex = 0;
NewCredentials newCredentials;
Ui::PaymentMethodDetails ui;
};
@ -222,6 +223,7 @@ public:
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);
@ -254,7 +256,7 @@ private:
void processDetails(const MTPDpayments_paymentForm &data);
void processDetails(const MTPDpayments_paymentReceipt &data);
void processSavedInformation(const MTPDpaymentRequestedInfo &data);
void processSavedCredentials(
void addSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data);
void processAdditionalPaymentMethods(
const QVector<MTPPaymentFormMethod> &list);
@ -263,6 +265,7 @@ private:
void fillStripeNativeMethod(QJsonObject object);
void fillSmartGlocalNativeMethod(QJsonObject object);
void refreshPaymentMethodDetails();
void refreshSavedPaymentMethodDetails();
[[nodiscard]] QString defaultPhone() const;
[[nodiscard]] QString defaultCountry() const;

View File

@ -500,7 +500,9 @@ void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
};
add(
tr::lng_payments_payment_method(),
_method.title,
(_method.savedMethods.empty()
? QString()
: _method.savedMethods[_method.savedMethodIndex].title),
&st::paymentsIconPaymentMethod,
[=] { _delegate->panelEditPaymentMethod(); });
if (_invoice.isShippingAddressRequested) {

View File

@ -227,7 +227,9 @@ void Panel::showForm(
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options) {
if (invoice && !method.ready && !method.native.supported) {
if (invoice
&& method.savedMethods.empty()
&& !method.native.supported) {
const auto available = Webview::Availability();
if (available.error != Webview::Available::Error::None) {
showWebviewError(
@ -410,25 +412,35 @@ void Panel::chooseTips(const Invoice &invoice) {
}
void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
auto bottomText = method.canSaveInformation
? rpl::producer<QString>()
: tr::lng_payments_processed_by(
lt_provider,
rpl::single(method.provider));
setTitle(tr::lng_payments_card_title());
if (method.native.supported) {
showEditCard(method.native, CardField::Number);
} else if (!showWebview(method.url, true, std::move(bottomText))) {
} 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_card(tr::now),
tr::lng_payments_webview_no_use(tr::now),
available);
} else {
showCriticalError({ "Error: Could not initialize WebView." });
}
_widget->setBackAllowed(true);
} else if (method.canSaveInformation) {
} else if (canSaveInformation) {
const auto &padding = st::paymentsPanelPadding;
_saveWebviewInformation = CreateChild<Checkbox>(
_webviewBottom.get(),
@ -444,23 +456,11 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
}
void Panel::showAdditionalMethod(
const PaymentMethodAdditional &method,
const QString &provider,
const PaymentMethodAdditional &method) {
auto bottomText = tr::lng_payments_processed_by(
lt_provider,
rpl::single(provider));
bool canSaveInformation) {
setTitle(rpl::single(method.title));
if (!showWebview(method.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);
}
showEditCardByUrl(method.url, provider, canSaveInformation);
}
void Panel::showWebviewProgress() {
@ -592,29 +592,32 @@ postEvent: function(eventType, eventData) {
}
void Panel::choosePaymentMethod(const PaymentMethodDetails &method) {
const auto hasSaved = method.ready;
if (!hasSaved && method.additionalMethods.empty()) {
if (method.savedMethods.empty() && method.additionalMethods.empty()) {
showEditPaymentMethod(method);
return;
}
showBox(Box([=](not_null<GenericBox*> box) {
const auto save = [=](int option) {
const auto basic = hasSaved ? 1 : 0;
if (option > basic) {
const auto index = option - basic - 1;
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.additionalMethods[index]);
} else if (!option) {
showEditPaymentMethod(method);
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),
};
if (hasSaved) {
options.push_back(method.title);
for (const auto &saved : method.savedMethods) {
options.push_back(saved.title);
}
for (const auto &additional : method.additionalMethods) {
options.push_back(additional.title);
@ -622,7 +625,9 @@ void Panel::choosePaymentMethod(const PaymentMethodDetails &method) {
SingleChoiceBox(box, {
.title = tr::lng_payments_payment_method(),
.options = std::move(options),
.initialSelection = hasSaved ? 1 : -1,
.initialSelection = (method.savedMethods.empty()
? -1
: (method.savedMethodIndex + 1)),
.callback = save,
});
}));
@ -823,6 +828,10 @@ 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();

View File

@ -63,14 +63,15 @@ public:
InformationField field);
void showEditPaymentMethod(const PaymentMethodDetails &method);
void showAdditionalMethod(
const PaymentMethodAdditional &method,
const QString &provider,
const PaymentMethodAdditional &method);
void showEditCard(
const NativeMethodDetails &native,
CardField field);
void showCardError(
const NativeMethodDetails &native,
CardField field);
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);
@ -86,6 +87,7 @@ public:
void updateThemeParams(const Webview::ThemeParams &params);
[[nodiscard]] rpl::producer<> backRequests() const;
[[nodiscard]] rpl::producer<QString> savedMethodChosen() const;
void showBox(object_ptr<Ui::BoxContent> box);
void showToast(const TextWithEntities &text);
@ -120,6 +122,7 @@ private:
rpl::variable<int> _formScrollTop;
QPointer<EditInformation> _weakEditInformation;
QPointer<EditCard> _weakEditCard;
rpl::event_stream<QString> _savedMethodChosen;
bool _webviewProgress = false;
bool _testMode = false;

View File

@ -167,13 +167,18 @@ struct PaymentMethodAdditional {
QString url;
};
struct PaymentMethodDetails {
struct PaymentMethodSaved {
QString id;
QString title;
};
struct PaymentMethodDetails {
NativeMethodDetails native;
std::vector<PaymentMethodSaved> savedMethods;
std::vector<PaymentMethodAdditional> additionalMethods;
QString url;
QString provider;
bool ready = false;
int savedMethodIndex = 0;
bool canSaveInformation = false;
};