/* 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 "main/main_domain.h" #include "storage/storage_domain.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 "webview/webview_interface.h" #include #include #include #include namespace Payments { namespace { struct SessionProcesses { base::flat_map> byItem; base::flat_map> bySlug; base::flat_map paymentStartedByItem; base::flat_map paymentStartedBySlug; rpl::lifetime lifetime; }; base::flat_map, SessionProcesses> Processes; [[nodiscard]] SessionProcesses &LookupSessionProcesses( not_null 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::start_with_next([=] { Processes.erase(session); }, result.lifetime); return result; } } // namespace void CheckoutProcess::Start( not_null item, Mode mode, Fn reactivate) { 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->requestActivate(); return; } const auto j = processes.byItem.emplace( id, std::make_unique( InvoiceId{ InvoiceMessage{ item->history()->peer, id.msg } }, mode, std::move(reactivate), PrivateTag{})).first; j->second->requestActivate(); } void CheckoutProcess::Start( not_null session, const QString &slug, Fn reactivate) { auto &processes = LookupSessionProcesses(session); const auto i = processes.bySlug.find(slug); if (i != end(processes.bySlug)) { i->second->setReactivateCallback(std::move(reactivate)); i->second->requestActivate(); return; } const auto j = processes.bySlug.emplace( slug, std::make_unique( InvoiceId{ InvoiceSlug{ session, slug } }, Mode::Payment, std::move(reactivate), PrivateTag{})).first; j->second->requestActivate(); } std::optional CheckoutProcess::InvoicePaid( not_null 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()) { Processes.erase(i); } return result; } std::optional CheckoutProcess::InvoicePaid( not_null 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()) { Processes.erase(i); } return result; } void CheckoutProcess::ClearAll() { Processes.clear(); } void CheckoutProcess::RegisterPaymentStart( not_null 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; } } } void CheckoutProcess::UnregisterPaymentStart( not_null 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; } } if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() && i->second.bySlug.empty()) { Processes.erase(i); } } CheckoutProcess::CheckoutProcess( InvoiceId id, Mode mode, Fn reactivate, PrivateTag) : _session(SessionFromId(id)) , _form(std::make_unique
(id, (mode == Mode::Receipt))) , _panel(std::make_unique(panelDelegate())) , _reactivate(std::move(reactivate)) { _form->updates( ) | rpl::start_with_next([=](const FormUpdate &update) { handleFormUpdate(update); }, _lifetime); _panel->savedMethodChosen( ) | rpl::start_with_next([=](QString id) { _form->chooseSavedMethod(id); }, _panel->lifetime()); _panel->backRequests( ) | rpl::start_with_next([=] { panelCancelEdit(); }, _panel->lifetime()); showForm(); _panel->toggleProgress(true); style::PaletteChanged( ) | rpl::filter([=] { return !_themeUpdateScheduled; }) | rpl::start_with_next([=] { _themeUpdateScheduled = true; crl::on_main(this, [=] { _themeUpdateScheduled = false; _panel->updateThemeParams(Window::Theme::WebViewParams()); }); }, _panel->lifetime()); if (mode == Mode::Payment) { _session->api().cloudPassword().state( ) | rpl::start_with_next([=](const Core::CloudPasswordState &state) { _form->setHasPassword(state.hasPassword); }, _lifetime); } } CheckoutProcess::~CheckoutProcess() { } void CheckoutProcess::setReactivateCallback( Fn reactivate) { _reactivate = std::move(reactivate); } void CheckoutProcess::requestActivate() { _panel->requestActivate(); } not_null CheckoutProcess::panelDelegate() { return static_cast(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.data()) { 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 Error &error) { handleError(error); }); } void CheckoutProcess::handleError(const Error &error) { const auto showToast = [&](const TextWithEntities &text) { _panel->requestActivate(); _panel->showToast(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.data()) { if (!box->handleCustomCheckError(id)) { showToast({ "Error: Could not generate tmp password." }); } } break; case Error::Type::Send: _sendFormFailed = true; if (const auto box = _enterPasswordBox.data()) { 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"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); } if (entry.byItem.empty() && entry.bySlug.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.isRecurring && !_form->details().termsAccepted) { _panel->requestTermsAcceptance( _form->details().termsBotUsername, invoice.recurringTermsUrl); } 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 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()); } 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) { _form->submit(result); }; auto owned = Box(_session, fields); _enterPasswordBox = owned.data(); _panel->showBox(std::move(owned)); }); } void CheckoutProcess::panelSetPassword() { getPasswordState([=](const Core::CloudPasswordState &state) { if (state.hasPassword) { return; } auto owned = Box( _session, PasscodeBox::CloudFields::From(state)); const auto box = owned.data(); rpl::merge( box->newPasswordSet() | rpl::to_empty, box->passwordReloadNeeded() ) | rpl::start_with_next([=] { _session->api().cloudPassword().reload(); }, box->lifetime()); box->clearUnconfirmedPassword( ) | rpl::start_with_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 callback) { Expects(callback != nullptr); if (_gettingPasswordState) { return; } _session->api().cloudPassword().state( ) | rpl::start_with_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 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); } QString CheckoutProcess::panelWebviewDataPath() { return _session->domain().local().webviewDataPath(); } } // namespace Payments