Add better error reporting to payments.

This commit is contained in:
John Preston 2021-04-02 19:18:49 +04:00
parent e106bd143e
commit ee098d00ad
12 changed files with 161 additions and 52 deletions

View file

@ -1855,6 +1855,12 @@ 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 4 (webkit2gtk-4.0) using your package manager.";
"lng_payments_webview_switch_mutter" = "Qt's window embedding doesn't work well with Mutter window manager. Please switch to another window manager or desktop environment.";
"lng_payments_webview_switch_wayland" = "There is no way to embed WebView window on Wayland. Please switch to X11.";
"lng_payments_sure_close" = "Are you sure you want to close this payment form? The changes you made will be lost.";
"lng_payments_receipt_label" = "Receipt";
"lng_payments_receipt_label_test" = "Test receipt";
@ -1903,6 +1909,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_tips_box_title" = "Add Tip";
"lng_payments_tips_max" = "Max possible tip amount: {amount}";
"lng_payments_shipping_not_available" = "Shipping to the selected country is not available.";
"lng_payments_card_declined" = "Your card was declined.";
"lng_payments_payment_failed" = "Payment failed. Your card has not been billed.";
"lng_payments_precheckout_failed" = "The bot couldn't process your payment. Your card has not been billed.";
"lng_payments_already_paid" = "You have already paid for this item.";
"lng_call_status_incoming" = "is calling you...";
"lng_call_status_connecting" = "connecting...";
"lng_call_status_exchanging" = "exchanging encryption keys...";

View file

@ -91,7 +91,6 @@ constexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60);
*data.vphoto(),
ImageLocation())
: nullptr),
.isMultipleAllowed = item->history()->isChannel(), // #TODO payments
.isTest = data.is_test(),
};
}
@ -189,10 +188,6 @@ PollData *Media::poll() const {
return nullptr;
}
void Media::setInvoiceReceiptId(MsgId id) {
Unexpected("Media::setInvoiceReceiptId.");
}
bool Media::uploading() const {
return false;
}
@ -1195,11 +1190,6 @@ const Invoice *MediaInvoice::invoice() const {
return &_invoice;
}
void MediaInvoice::setInvoiceReceiptId(MsgId id) {
_invoice.receiptMsgId = id;
parent()->checkBuyButton();
}
bool MediaInvoice::hasReplyPreview() const {
if (const auto photo = _invoice.photo) {
return !photo->isNull();

View file

@ -62,7 +62,6 @@ struct Invoice {
QString title;
QString description;
PhotoData *photo = nullptr;
bool isMultipleAllowed = false;
bool isTest = false;
};
@ -85,8 +84,6 @@ public:
virtual Data::CloudImage *location() const;
virtual PollData *poll() const;
virtual void setInvoiceReceiptId(MsgId id);
virtual bool uploading() const;
virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
virtual bool canBeGrouped() const;
@ -384,7 +381,6 @@ public:
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
const Invoice *invoice() const override;
void setInvoiceReceiptId(MsgId id) override;
bool hasReplyPreview() const override;
Image *replyPreview() const override;

View file

@ -776,16 +776,9 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() {
if (payment->msg) {
if (const auto media = payment->msg->media()) {
if (const auto invoice = media->invoice()) {
if (!invoice->isMultipleAllowed
&& !invoice->receiptMsgId) {
media->setInvoiceReceiptId(id);
}
return textcmdLink(1, invoice->title);
}
}
return QString();// tr::lng_deleted_message(tr::now);
} else if (payment->msgId) {
return QString();// tr::lng_contacts_loading(tr::now);
}
return QString();
}();

View file

@ -199,10 +199,14 @@ void CheckoutProcess::handleError(const Error &error) {
const auto &id = error.id;
switch (error.type) {
case Error::Type::Form:
if (true
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) {
showToast({ "Error: " + id });
_panel->showCriticalError({ "Error: " + id });
}
break;
case Error::Type::Validate: {
@ -246,9 +250,9 @@ void CheckoutProcess::handleError(const Error &error) {
} 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!" }); // #TODO payments errors message
showToast({ "Error: Bot Timeout!" });
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
showToast({ "Error: Shipping to the selected country is not available!" }); // #TODO payments errors message
showToast({ tr::lng_payments_shipping_not_available(tr::now) });
} else {
showToast({ "Error: " + id });
}
@ -264,11 +268,9 @@ void CheckoutProcess::handleError(const Error &error) {
|| id == u"ExpiredCard"_q) {
showCardError(Field::ExpireDate);
} else if (id == u"CardDeclined"_q) {
// #TODO payments errors message
showToast({ "Error: " + id });
showToast({ tr::lng_payments_card_declined(tr::now) });
} else if (id == u"ProcessingError"_q) {
// #TODO payments errors message
showToast({ "Error: " + id });
showToast({ "Sorry, a processing error occurred." });
} else {
showToast({ "Error: " + id });
}
@ -287,17 +289,20 @@ void CheckoutProcess::handleError(const Error &error) {
if (_submitState == SubmitState::Finishing) {
_submitState = SubmitState::Validated;
}
if (id == u"PAYMENT_FAILED"_q) {
showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message
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({ "Error: PreCheckout Failed. Your card has not been billed." }); // #TODO payments errors message
showToast({ tr::lng_payments_precheckout_failed(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) {
// #TODO payments save
requestPassword();
} else {
showToast({ "Error: " + id });
}
@ -574,6 +579,10 @@ void CheckoutProcess::panelSetPassword() {
});
}
void CheckoutProcess::panelOpenUrl(const QString &url) {
File::OpenUrl(url);
}
void CheckoutProcess::getPasswordState(
Fn<void(const Core::CloudPasswordState&)> callback) {
Expects(callback != nullptr);

View file

@ -102,6 +102,7 @@ private:
bool saveInformation) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelSetPassword() override;
void panelOpenUrl(const QString &url) override;
void panelCancelEdit() override;
void panelEditPaymentMethod() override;

View file

@ -9,6 +9,8 @@ using "ui/basic.style";
using "info/info.style";
paymentsPanelSize: size(392px, 600px);
paymentsPanelButton: defaultBoxButton;
paymentsPanelSubmit: RoundButton(defaultActiveButton) {
width: -36px;
@ -116,3 +118,10 @@ paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px);
paymentsToProviderLabel: paymentsShippingPrice;
paymentsToProviderPadding: margins(28px, 6px, 28px, 6px);
paymentsCriticalError: FlatLabel(boxLabel) {
minWidth: 370px;
align: align(top);
textFg: windowSubTextFg;
}
paymentsCriticalErrorPadding: margins(10px, 40px, 10px, 0px);

View file

@ -76,6 +76,7 @@ FormSummary::FormSummary(
, _options(options)
, _information(current)
, _scroll(this, st::passportPanelScroll)
, _layout(_scroll->setOwnedWidget(object_ptr<VerticalLayout>(this)))
, _topShadow(this)
, _bottomShadow(this)
, _submit(_invoice.receipt.paid
@ -114,6 +115,20 @@ 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;
}
Settings::AddSkip(_layout.get(), st::paymentsPricesTopSkip);
_layout->add(object_ptr<FlatLabel>(
_layout.get(),
rpl::single(text),
st::paymentsCriticalError));
return true;
}
void FormSummary::updateThumbnail(const QImage &thumbnail) {
_invoice.cover.thumbnail = thumbnail;
_thumbnails.fire_copy(thumbnail);
@ -149,7 +164,7 @@ int64 FormSummary::computeTotalAmount() const {
}
void FormSummary::setupControls() {
const auto inner = setupContent();
setupContent(_layout.get());
if (_submit) {
_submit->addClickHandler([=] {
@ -173,7 +188,7 @@ void FormSummary::setupControls() {
_bottomShadow->toggleOn(rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
inner->heightValue(),
_layout->heightValue(),
_1 + _2 < _3));
}
@ -533,24 +548,19 @@ void FormSummary::setupSections(not_null<VerticalLayout*> layout) {
Settings::AddSkip(layout, st::paymentsSectionsTopSkip);
}
not_null<RpWidget*> FormSummary::setupContent() {
const auto inner = _scroll->setOwnedWidget(
object_ptr<VerticalLayout>(this));
void FormSummary::setupContent(not_null<VerticalLayout*> layout) {
_scroll->widthValue(
) | rpl::start_with_next([=](int width) {
inner->resizeToWidth(width);
}, inner->lifetime());
layout->resizeToWidth(width);
}, layout->lifetime());
setupCover(inner);
setupCover(layout);
if (_invoice) {
Settings::AddDivider(inner);
setupPrices(inner);
Settings::AddDivider(inner);
setupSections(inner);
Settings::AddDivider(layout);
setupPrices(layout);
Settings::AddDivider(layout);
setupSections(layout);
}
return inner;
}
void FormSummary::resizeEvent(QResizeEvent *e) {

View file

@ -39,11 +39,13 @@ public:
void updateThumbnail(const QImage &thumbnail);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
bool showCriticalError(const TextWithEntities &text);
private:
void resizeEvent(QResizeEvent *e) override;
void setupControls();
[[nodiscard]] not_null<Ui::RpWidget*> setupContent();
void setupContent(not_null<VerticalLayout*> layout);
void setupCover(not_null<VerticalLayout*> layout);
void setupPrices(not_null<VerticalLayout*> layout);
void setupSuggestedTips(not_null<VerticalLayout*> layout);
@ -61,6 +63,7 @@ private:
ShippingOptions _options;
RequestedInformation _information;
object_ptr<ScrollArea> _scroll;
not_null<VerticalLayout*> _layout;
object_ptr<FadeShadow> _topShadow;
object_ptr<FadeShadow> _bottomShadow;
object_ptr<RoundButton> _submit;

View file

@ -17,10 +17,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/fade_wrap.h"
#include "ui/boxes/single_choice_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "lang/lang_keys.h"
#include "webview/webview_embed.h"
#include "webview/webview_interface.h"
#include "styles/style_payments.h"
#include "styles/style_passport.h"
#include "styles/style_layers.h"
namespace Payments::Ui {
@ -28,7 +29,7 @@ namespace Payments::Ui {
Panel::Panel(not_null<PanelDelegate*> delegate)
: _delegate(delegate)
, _widget(std::make_unique<SeparatePanel>()) {
_widget->setInnerSize(st::passportPanelSize);
_widget->setInnerSize(st::paymentsPanelSize);
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
_widget->closeRequests(
@ -56,6 +57,16 @@ void Panel::showForm(
const RequestedInformation &current,
const PaymentMethodDetails &method,
const ShippingOptions &options) {
if (invoice && !method.ready && !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()
@ -250,7 +261,15 @@ void Panel::showEditPaymentMethod(const PaymentMethodDetails &method) {
if (method.native.supported) {
showEditCard(method.native, CardField::Number);
} else if (!showWebview(method.url, true, std::move(bottomText))) {
// #TODO payments errors not supported
const auto available = Webview::Availability();
if (available.error != Webview::Available::Error::None) {
showWebviewError(
tr::lng_payments_webview_no_card(tr::now),
available);
} else {
showCriticalError({ "Error: Could not initialize WebView." });
}
_widget->setBackAllowed(true);
} else if (method.canSaveInformation) {
const auto &padding = st::paymentsPanelPadding;
_saveWebviewInformation = CreateChild<Checkbox>(
@ -487,6 +506,67 @@ void Panel::showToast(const TextWithEntities &text) {
_widget->showToast(text);
}
void Panel::showCriticalError(const TextWithEntities &text) {
if (!_weakFormSummary || !_weakFormSummary->showCriticalError(text)) {
auto error = base::make_unique_q<PaddingWrap<FlatLabel>>(
_widget.get(),
object_ptr<FlatLabel>(
_widget.get(),
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;
});
_widget->showInner(std::move(error));
}
}
void Panel::showWebviewError(
const QString &text,
const Webview::Available &information) {
using Error = Webview::Available::Error;
Expects(information.error != Error::None);
auto rich = TextWithEntities{ text };
rich.append("\n\n");
switch (information.error) {
case Error::NoWebview2: {
const auto command = QString(QChar(TextCommand));
const auto text = tr::lng_payments_webview_install_edge(
tr::now,
lt_link,
command);
const auto parts = text.split(command);
rich.append(parts.value(0))
.append(Text::Link(
"Microsoft Edge WebView2 Runtime",
"https://go.microsoft.com/fwlink/p/?LinkId=2124703"))
.append(parts.value(1));
} break;
case Error::NoGtkOrWebkit2Gtk:
rich.append(tr::lng_payments_webview_install_webkit(tr::now));
break;
case Error::MutterWM:
rich.append(tr::lng_payments_webview_switch_mutter(tr::now));
break;
case Error::Wayland:
rich.append(tr::lng_payments_webview_switch_wayland(tr::now));
break;
default:
rich.append(QString::fromStdString(information.details));
break;
}
showCriticalError(rich);
}
rpl::lifetime &Panel::lifetime() {
return _widget->lifetime();
}

View file

@ -18,6 +18,7 @@ class Checkbox;
namespace Webview {
class Window;
struct Available;
} // namespace Webview
namespace Payments::Ui {
@ -80,11 +81,15 @@ public:
void showBox(object_ptr<Ui::BoxContent> box);
void showToast(const TextWithEntities &text);
void showCriticalError(const TextWithEntities &text);
[[nodiscard]] rpl::lifetime &lifetime();
private:
bool createWebview();
void showWebviewError(
const QString &text,
const Webview::Available &information);
void setTitle(rpl::producer<QString> title);
const not_null<PanelDelegate*> _delegate;

View file

@ -34,6 +34,7 @@ public:
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;