| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "modules/payments/PaymentRequest.h" |
| |
| #include <stddef.h> |
| #include <utility> |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "bindings/core/v8/ScriptPromiseResolver.h" |
| #include "bindings/core/v8/V8StringResource.h" |
| #include "bindings/modules/v8/V8AndroidPayMethodData.h" |
| #include "bindings/modules/v8/V8BasicCardRequest.h" |
| #include "bindings/modules/v8/V8PaymentDetailsUpdate.h" |
| #include "core/EventTypeNames.h" |
| #include "core/dom/DOMException.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "core/dom/TaskRunnerHelper.h" |
| #include "core/events/Event.h" |
| #include "core/events/EventQueue.h" |
| #include "core/frame/Deprecation.h" |
| #include "core/frame/FrameOwner.h" |
| #include "core/html/HTMLIFrameElement.h" |
| #include "core/inspector/ConsoleMessage.h" |
| #include "core/inspector/ConsoleTypes.h" |
| #include "modules/EventTargetModulesNames.h" |
| #include "modules/payments/AndroidPayMethodData.h" |
| #include "modules/payments/AndroidPayTokenization.h" |
| #include "modules/payments/BasicCardRequest.h" |
| #include "modules/payments/HTMLIFrameElementPayments.h" |
| #include "modules/payments/PaymentAddress.h" |
| #include "modules/payments/PaymentDetailsInit.h" |
| #include "modules/payments/PaymentDetailsUpdate.h" |
| #include "modules/payments/PaymentItem.h" |
| #include "modules/payments/PaymentRequestUpdateEvent.h" |
| #include "modules/payments/PaymentResponse.h" |
| #include "modules/payments/PaymentShippingOption.h" |
| #include "modules/payments/PaymentsValidators.h" |
| #include "mojo/public/cpp/bindings/interface_request.h" |
| #include "platform/RuntimeEnabledFeatures.h" |
| #include "platform/UUID.h" |
| #include "platform/bindings/ScriptState.h" |
| #include "platform/feature_policy/FeaturePolicy.h" |
| #include "platform/mojo/MojoHelper.h" |
| #include "platform/wtf/HashSet.h" |
| #include "public/platform/InterfaceProvider.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/WebTraceLocation.h" |
| |
| namespace { |
| |
| using ::payments::mojom::blink::CanMakePaymentQueryResult; |
| using ::payments::mojom::blink::PaymentAddressPtr; |
| using ::payments::mojom::blink::PaymentCurrencyAmount; |
| using ::payments::mojom::blink::PaymentCurrencyAmountPtr; |
| using ::payments::mojom::blink::PaymentDetailsModifierPtr; |
| using ::payments::mojom::blink::PaymentDetailsPtr; |
| using ::payments::mojom::blink::PaymentErrorReason; |
| using ::payments::mojom::blink::PaymentItemPtr; |
| using ::payments::mojom::blink::PaymentMethodDataPtr; |
| using ::payments::mojom::blink::PaymentOptionsPtr; |
| using ::payments::mojom::blink::PaymentResponsePtr; |
| using ::payments::mojom::blink::PaymentShippingOptionPtr; |
| using ::payments::mojom::blink::PaymentShippingType; |
| |
| } // namespace |
| |
| namespace mojo { |
| |
| template <> |
| struct TypeConverter<PaymentCurrencyAmountPtr, blink::PaymentCurrencyAmount> { |
| static PaymentCurrencyAmountPtr Convert( |
| const blink::PaymentCurrencyAmount& input) { |
| PaymentCurrencyAmountPtr output = PaymentCurrencyAmount::New(); |
| output->currency = input.currency(); |
| output->value = input.value(); |
| output->currency_system = input.currencySystem(); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentItemPtr, blink::PaymentItem> { |
| static PaymentItemPtr Convert(const blink::PaymentItem& input) { |
| PaymentItemPtr output = payments::mojom::blink::PaymentItem::New(); |
| output->label = input.label(); |
| output->amount = PaymentCurrencyAmount::From(input.amount()); |
| output->pending = input.pending(); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentShippingOptionPtr, blink::PaymentShippingOption> { |
| static PaymentShippingOptionPtr Convert( |
| const blink::PaymentShippingOption& input) { |
| PaymentShippingOptionPtr output = |
| payments::mojom::blink::PaymentShippingOption::New(); |
| output->id = input.id(); |
| output->label = input.label(); |
| output->amount = PaymentCurrencyAmount::From(input.amount()); |
| output->selected = input.hasSelected() && input.selected(); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentOptionsPtr, blink::PaymentOptions> { |
| static PaymentOptionsPtr Convert(const blink::PaymentOptions& input) { |
| PaymentOptionsPtr output = payments::mojom::blink::PaymentOptions::New(); |
| output->request_payer_name = input.requestPayerName(); |
| output->request_payer_email = input.requestPayerEmail(); |
| output->request_payer_phone = input.requestPayerPhone(); |
| output->request_shipping = input.requestShipping(); |
| |
| if (input.shippingType() == "delivery") |
| output->shipping_type = PaymentShippingType::DELIVERY; |
| else if (input.shippingType() == "pickup") |
| output->shipping_type = PaymentShippingType::PICKUP; |
| else |
| output->shipping_type = PaymentShippingType::SHIPPING; |
| |
| return output; |
| } |
| }; |
| |
| } // namespace mojo |
| |
| namespace blink { |
| namespace { |
| |
| using ::payments::mojom::blink::BasicCardNetwork; |
| |
| const struct { |
| const BasicCardNetwork code; |
| const char* const name; |
| } kBasicCardNetworks[] = {{BasicCardNetwork::AMEX, "amex"}, |
| {BasicCardNetwork::DINERS, "diners"}, |
| {BasicCardNetwork::DISCOVER, "discover"}, |
| {BasicCardNetwork::JCB, "jcb"}, |
| {BasicCardNetwork::MASTERCARD, "mastercard"}, |
| {BasicCardNetwork::MIR, "mir"}, |
| {BasicCardNetwork::UNIONPAY, "unionpay"}, |
| {BasicCardNetwork::VISA, "visa"}}; |
| |
| // If the website does not call complete() 60 seconds after show() has been |
| // resolved, then behave as if the website called complete("fail"). |
| static const int kCompleteTimeoutSeconds = 60; |
| |
| // Validates ShippingOption or PaymentItem, which happen to have identical |
| // fields, except for "id", which is present only in ShippingOption. |
| template <typename T> |
| void ValidateShippingOptionOrPaymentItem(const T& item, |
| const String& item_name, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| DCHECK(item.hasLabel()); |
| DCHECK(item.hasAmount()); |
| DCHECK(item.amount().hasValue()); |
| DCHECK(item.amount().hasCurrency()); |
| |
| String error_message; |
| if (!PaymentsValidators::IsValidAmountFormat(item.amount().value(), item_name, |
| &error_message)) { |
| exception_state.ThrowTypeError(error_message); |
| return; |
| } |
| |
| if (item.label().IsEmpty()) { |
| execution_context.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kErrorMessageLevel, |
| "Empty " + item_name + " label may be confusing the user")); |
| return; |
| } |
| |
| if (!PaymentsValidators::IsValidCurrencyCodeFormat( |
| item.amount().currency(), item.amount().currencySystem(), |
| &error_message)) { |
| exception_state.ThrowTypeError(error_message); |
| return; |
| } |
| } |
| |
| void ValidateAndConvertDisplayItems(const HeapVector<PaymentItem>& input, |
| const String& item_names, |
| Vector<PaymentItemPtr>& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| for (const PaymentItem& item : input) { |
| ValidateShippingOptionOrPaymentItem(item, item_names, execution_context, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| output.push_back(payments::mojom::blink::PaymentItem::From(item)); |
| } |
| } |
| |
| // Validates and converts |input| shipping options into |output|. Throws an |
| // exception if the data is not valid, except for duplicate identifiers, which |
| // returns an empty |output| instead of throwing an exception. There's no need |
| // to clear |output| when an exception is thrown, because the caller takes care |
| // of deleting |output|. |
| void ValidateAndConvertShippingOptions( |
| const HeapVector<PaymentShippingOption>& input, |
| Vector<PaymentShippingOptionPtr>& output, |
| String& shipping_option_output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| HashSet<String> unique_ids; |
| for (const PaymentShippingOption& option : input) { |
| ValidateShippingOptionOrPaymentItem(option, "shippingOptions", |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| DCHECK(option.hasId()); |
| if (option.id().IsEmpty()) { |
| execution_context.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kWarningMessageLevel, |
| "Empty shipping option ID may be hard to debug")); |
| return; |
| } |
| |
| if (unique_ids.Contains(option.id())) { |
| execution_context.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kWarningMessageLevel, |
| "Duplicate shipping option identifier '" + option.id() + |
| "' is treated as an invalid address indicator.")); |
| // Clear |output| instead of throwing an exception. |
| output.clear(); |
| shipping_option_output = String(); |
| return; |
| } |
| |
| if (option.selected()) |
| shipping_option_output = option.id(); |
| |
| unique_ids.insert(option.id()); |
| |
| output.push_back( |
| payments::mojom::blink::PaymentShippingOption::From(option)); |
| } |
| } |
| |
| void ValidateAndConvertTotal(const PaymentItem& input, |
| const String& item_name, |
| PaymentItemPtr& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| ValidateShippingOptionOrPaymentItem(input, item_name, execution_context, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| if (input.amount().value()[0] == '-') { |
| exception_state.ThrowTypeError("Total amount value should be non-negative"); |
| return; |
| } |
| |
| output = payments::mojom::blink::PaymentItem::From(input); |
| } |
| |
| // Parses Android Pay data to avoid parsing JSON in the browser. |
| void SetAndroidPayMethodData(const ScriptValue& input, |
| PaymentMethodDataPtr& output, |
| ExceptionState& exception_state) { |
| AndroidPayMethodData android_pay; |
| V8AndroidPayMethodData::toImpl(input.GetIsolate(), input.V8Value(), |
| android_pay, exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| if (android_pay.hasEnvironment() && android_pay.environment() == "TEST") |
| output->environment = payments::mojom::blink::AndroidPayEnvironment::TEST; |
| |
| output->merchant_name = android_pay.merchantName(); |
| output->merchant_id = android_pay.merchantId(); |
| |
| // 0 means the merchant did not specify or it was an invalid value |
| output->min_google_play_services_version = 0; |
| if (android_pay.hasMinGooglePlayServicesVersion()) { |
| bool ok = false; |
| int min_google_play_services_version = |
| android_pay.minGooglePlayServicesVersion().ToIntStrict(&ok); |
| if (ok) { |
| output->min_google_play_services_version = |
| min_google_play_services_version; |
| } |
| } |
| |
| if (android_pay.hasAllowedCardNetworks()) { |
| using ::payments::mojom::blink::AndroidPayCardNetwork; |
| |
| const struct { |
| const AndroidPayCardNetwork code; |
| const char* const name; |
| } kAndroidPayNetwork[] = {{AndroidPayCardNetwork::AMEX, "AMEX"}, |
| {AndroidPayCardNetwork::DISCOVER, "DISCOVER"}, |
| {AndroidPayCardNetwork::MASTERCARD, "MASTERCARD"}, |
| {AndroidPayCardNetwork::VISA, "VISA"}}; |
| |
| for (const String& allowed_card_network : |
| android_pay.allowedCardNetworks()) { |
| for (size_t i = 0; i < arraysize(kAndroidPayNetwork); ++i) { |
| if (allowed_card_network == kAndroidPayNetwork[i].name) { |
| output->allowed_card_networks.push_back(kAndroidPayNetwork[i].code); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (android_pay.hasPaymentMethodTokenizationParameters()) { |
| const blink::AndroidPayTokenization& tokenization = |
| android_pay.paymentMethodTokenizationParameters(); |
| output->tokenization_type = |
| payments::mojom::blink::AndroidPayTokenization::UNSPECIFIED; |
| if (tokenization.hasTokenizationType()) { |
| using ::payments::mojom::blink::AndroidPayTokenization; |
| |
| const struct { |
| const AndroidPayTokenization code; |
| const char* const name; |
| } kAndroidPayTokenization[] = { |
| {AndroidPayTokenization::GATEWAY_TOKEN, "GATEWAY_TOKEN"}, |
| {AndroidPayTokenization::NETWORK_TOKEN, "NETWORK_TOKEN"}}; |
| |
| for (size_t i = 0; i < arraysize(kAndroidPayTokenization); ++i) { |
| if (tokenization.tokenizationType() == |
| kAndroidPayTokenization[i].name) { |
| output->tokenization_type = kAndroidPayTokenization[i].code; |
| break; |
| } |
| } |
| } |
| |
| if (tokenization.hasParameters()) { |
| const Vector<String>& keys = |
| tokenization.parameters().GetPropertyNames(exception_state); |
| if (exception_state.HadException()) |
| return; |
| String value; |
| for (const String& key : keys) { |
| if (!DictionaryHelper::Get(tokenization.parameters(), key, value)) |
| continue; |
| output->parameters.push_back( |
| payments::mojom::blink::AndroidPayTokenizationParameter::New()); |
| output->parameters.back()->key = key; |
| output->parameters.back()->value = value; |
| } |
| } |
| } |
| } |
| |
| // Parses basic-card data to avoid parsing JSON in the browser. |
| void SetBasicCardMethodData(const ScriptValue& input, |
| PaymentMethodDataPtr& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| BasicCardRequest basic_card; |
| V8BasicCardRequest::toImpl(input.GetIsolate(), input.V8Value(), basic_card, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| if (basic_card.hasSupportedNetworks()) { |
| for (const String& network : basic_card.supportedNetworks()) { |
| for (size_t i = 0; i < arraysize(kBasicCardNetworks); ++i) { |
| if (network == kBasicCardNetworks[i].name) { |
| output->supported_networks.push_back(kBasicCardNetworks[i].code); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (basic_card.hasSupportedTypes()) { |
| using ::payments::mojom::blink::BasicCardType; |
| |
| const struct { |
| const BasicCardType code; |
| const char* const name; |
| } kBasicCardTypes[] = {{BasicCardType::CREDIT, "credit"}, |
| {BasicCardType::DEBIT, "debit"}, |
| {BasicCardType::PREPAID, "prepaid"}}; |
| |
| for (const String& type : basic_card.supportedTypes()) { |
| for (size_t i = 0; i < arraysize(kBasicCardTypes); ++i) { |
| if (type == kBasicCardTypes[i].name) { |
| output->supported_types.push_back(kBasicCardTypes[i].code); |
| break; |
| } |
| } |
| } |
| |
| if (output->supported_types.size() != arraysize(kBasicCardTypes)) { |
| execution_context.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kWarningMessageLevel, |
| "Cannot yet distinguish credit, debit, and prepaid cards.")); |
| } |
| } |
| } |
| |
| void StringifyAndParseMethodSpecificData( |
| const Vector<String>& supported_methods, |
| const ScriptValue& input, |
| PaymentMethodDataPtr& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| DCHECK(!input.IsEmpty()); |
| v8::Local<v8::String> value; |
| if (!input.V8Value()->IsObject() || |
| !v8::JSON::Stringify(input.GetContext(), input.V8Value().As<v8::Object>()) |
| .ToLocal(&value)) { |
| exception_state.ThrowTypeError( |
| "Payment method data should be a JSON-serializable object"); |
| return; |
| } |
| |
| output->stringified_data = |
| V8StringToWebCoreString<String>(value, kDoNotExternalize); |
| |
| // Serialize payment method specific data to be sent to the payment apps. The |
| // payment apps are responsible for validating and processing their method |
| // data asynchronously. Do not throw exceptions here. |
| if (supported_methods.Contains("https://android.com/pay") || |
| supported_methods.Contains("https://google.com/pay")) { |
| SetAndroidPayMethodData(input, output, exception_state); |
| if (exception_state.HadException()) |
| exception_state.ClearException(); |
| } |
| if (RuntimeEnabledFeatures::paymentRequestBasicCardEnabled() && |
| supported_methods.Contains("basic-card")) { |
| SetBasicCardMethodData(input, output, execution_context, exception_state); |
| if (exception_state.HadException()) |
| exception_state.ClearException(); |
| } |
| |
| for (size_t i = 0; i < arraysize(kBasicCardNetworks); ++i) { |
| if (supported_methods.Contains(kBasicCardNetworks[i].name)) { |
| Deprecation::CountDeprecation( |
| &execution_context, |
| UseCounter::kPaymentRequestNetworkNameInSupportedMethods); |
| break; |
| } |
| } |
| } |
| |
| void ValidateAndConvertPaymentDetailsModifiers( |
| const HeapVector<PaymentDetailsModifier>& input, |
| Vector<PaymentDetailsModifierPtr>& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| for (const PaymentDetailsModifier& modifier : input) { |
| output.push_back(payments::mojom::blink::PaymentDetailsModifier::New()); |
| if (modifier.hasTotal()) { |
| ValidateAndConvertTotal(modifier.total(), "modifier total", |
| output.back()->total, execution_context, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| |
| if (modifier.hasAdditionalDisplayItems()) { |
| ValidateAndConvertDisplayItems(modifier.additionalDisplayItems(), |
| "additional display items in modifier", |
| output.back()->additional_display_items, |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| |
| if (modifier.supportedMethods().IsEmpty()) { |
| exception_state.ThrowTypeError( |
| "Must specify at least one payment method identifier"); |
| return; |
| } |
| |
| output.back()->method_data = |
| payments::mojom::blink::PaymentMethodData::New(); |
| output.back()->method_data->supported_methods = modifier.supportedMethods(); |
| |
| if (modifier.hasData() && !modifier.data().IsEmpty()) { |
| StringifyAndParseMethodSpecificData( |
| modifier.supportedMethods(), modifier.data(), |
| output.back()->method_data, execution_context, exception_state); |
| } else { |
| output.back()->method_data->stringified_data = ""; |
| } |
| } |
| } |
| |
| void ValidateAndConvertPaymentDetailsBase(const PaymentDetailsBase& input, |
| PaymentDetailsPtr& output, |
| String& shipping_option_output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| if (input.hasDisplayItems()) { |
| ValidateAndConvertDisplayItems(input.displayItems(), "display items", |
| output->display_items, execution_context, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| |
| if (input.hasShippingOptions()) { |
| ValidateAndConvertShippingOptions( |
| input.shippingOptions(), output->shipping_options, |
| shipping_option_output, execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| } else { |
| shipping_option_output = String(); |
| } |
| |
| if (input.hasModifiers()) { |
| ValidateAndConvertPaymentDetailsModifiers( |
| input.modifiers(), output->modifiers, execution_context, |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| } |
| |
| void ValidateAndConvertPaymentDetailsInit(const PaymentDetailsInit& input, |
| PaymentDetailsPtr& output, |
| String& shipping_option_output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| DCHECK(input.hasTotal()); |
| ValidateAndConvertTotal(input.total(), "total", output->total, |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| ValidateAndConvertPaymentDetailsBase(input, output, shipping_option_output, |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| |
| void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate& input, |
| PaymentDetailsPtr& output, |
| String& shipping_option_output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| ValidateAndConvertPaymentDetailsBase(input, output, shipping_option_output, |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| if (input.hasTotal()) { |
| ValidateAndConvertTotal(input.total(), "total", output->total, |
| execution_context, exception_state); |
| if (exception_state.HadException()) |
| return; |
| } |
| |
| if (input.hasError() && !input.error().IsNull()) { |
| String error_message; |
| if (!PaymentsValidators::IsValidErrorMsgFormat(input.error(), |
| &error_message)) { |
| exception_state.ThrowTypeError(error_message); |
| return; |
| } |
| output->error = input.error(); |
| } else { |
| output->error = ""; |
| } |
| } |
| |
| void ValidateAndConvertPaymentMethodData( |
| const HeapVector<PaymentMethodData>& input, |
| Vector<payments::mojom::blink::PaymentMethodDataPtr>& output, |
| ExecutionContext& execution_context, |
| ExceptionState& exception_state) { |
| if (input.IsEmpty()) { |
| exception_state.ThrowTypeError("At least one payment method is required"); |
| return; |
| } |
| |
| for (const PaymentMethodData payment_method_data : input) { |
| if (payment_method_data.supportedMethods().IsEmpty()) { |
| exception_state.ThrowTypeError( |
| "Each payment method needs to include at least one payment method " |
| "identifier"); |
| return; |
| } |
| |
| output.push_back(payments::mojom::blink::PaymentMethodData::New()); |
| output.back()->supported_methods = payment_method_data.supportedMethods(); |
| |
| if (payment_method_data.hasData() && |
| !payment_method_data.data().IsEmpty()) { |
| StringifyAndParseMethodSpecificData( |
| payment_method_data.supportedMethods(), payment_method_data.data(), |
| output.back(), execution_context, exception_state); |
| } else { |
| output.back()->stringified_data = ""; |
| } |
| } |
| } |
| |
| bool AllowedToUsePaymentRequest(const Frame* frame) { |
| // To determine whether a Document object |document| is allowed to use the |
| // feature indicated by attribute name |allowpaymentrequest|, run these steps: |
| |
| // 1. If |document| has no browsing context, then return false. |
| if (!frame) |
| return false; |
| |
| if (!IsSupportedInFeaturePolicy(WebFeaturePolicyFeature::kPayment)) { |
| // 2. If |document|'s browsing context is a top-level browsing context, then |
| // return true. |
| if (frame->IsMainFrame()) |
| return true; |
| |
| // 3. If |document|'s browsing context has a browsing context container that |
| // is an iframe element with an |allowpaymentrequest| attribute specified, |
| // and whose node document is allowed to use the feature indicated by |
| // |allowpaymentrequest|, then return true. |
| if (frame->Owner() && frame->Owner()->AllowPaymentRequest()) |
| return AllowedToUsePaymentRequest(frame->Tree().Parent()); |
| |
| // 4. Return false. |
| return false; |
| } |
| |
| // 2. If Feature Policy is enabled, return the policy for "payment" feature. |
| return frame->IsFeatureEnabled(WebFeaturePolicyFeature::kPayment); |
| } |
| |
| void WarnIgnoringQueryQuotaForCanMakePayment( |
| ExecutionContext& execution_context) { |
| execution_context.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kWarningMessageLevel, |
| "Quota reached for PaymentRequest.canMakePayment(). This would normally " |
| "reject the promise, but allowing continued usage on localhost and " |
| "file:// scheme origins.")); |
| } |
| |
| } // namespace |
| |
| PaymentRequest* PaymentRequest::Create( |
| ExecutionContext* execution_context, |
| const HeapVector<PaymentMethodData>& method_data, |
| const PaymentDetailsInit& details, |
| ExceptionState& exception_state) { |
| return new PaymentRequest(execution_context, method_data, details, |
| PaymentOptions(), exception_state); |
| } |
| |
| PaymentRequest* PaymentRequest::Create( |
| ExecutionContext* execution_context, |
| const HeapVector<PaymentMethodData>& method_data, |
| const PaymentDetailsInit& details, |
| const PaymentOptions& options, |
| ExceptionState& exception_state) { |
| return new PaymentRequest(execution_context, method_data, details, options, |
| exception_state); |
| } |
| |
| PaymentRequest::~PaymentRequest() {} |
| |
| ScriptPromise PaymentRequest::show(ScriptState* script_state) { |
| if (!payment_provider_.is_bound() || show_resolver_) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create(kInvalidStateError, "Already called show() once")); |
| } |
| |
| if (!script_state->ContextIsValid() || !LocalDOMWindow::From(script_state) || |
| !LocalDOMWindow::From(script_state)->GetFrame()) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, DOMException::Create(kInvalidStateError, |
| "Cannot show the payment request")); |
| } |
| |
| payment_provider_->Show(); |
| |
| show_resolver_ = ScriptPromiseResolver::Create(script_state); |
| return show_resolver_->Promise(); |
| } |
| |
| ScriptPromise PaymentRequest::abort(ScriptState* script_state) { |
| if (!script_state->ContextIsValid()) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create(kInvalidStateError, "Cannot abort payment")); |
| } |
| |
| if (abort_resolver_) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create(kInvalidStateError, |
| "Cannot abort() again until the previous abort() " |
| "has resolved or rejected")); |
| } |
| |
| if (!show_resolver_) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create(kInvalidStateError, |
| "Never called show(), so nothing to abort")); |
| } |
| |
| abort_resolver_ = ScriptPromiseResolver::Create(script_state); |
| payment_provider_->Abort(); |
| return abort_resolver_->Promise(); |
| } |
| |
| ScriptPromise PaymentRequest::canMakePayment(ScriptState* script_state) { |
| if (!payment_provider_.is_bound() || can_make_payment_resolver_ || |
| !script_state->ContextIsValid()) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, DOMException::Create(kInvalidStateError, |
| "Cannot query payment request")); |
| } |
| |
| payment_provider_->CanMakePayment(); |
| |
| can_make_payment_resolver_ = ScriptPromiseResolver::Create(script_state); |
| return can_make_payment_resolver_->Promise(); |
| } |
| |
| bool PaymentRequest::HasPendingActivity() const { |
| return show_resolver_ || complete_resolver_; |
| } |
| |
| const AtomicString& PaymentRequest::InterfaceName() const { |
| return EventTargetNames::PaymentRequest; |
| } |
| |
| ExecutionContext* PaymentRequest::GetExecutionContext() const { |
| return ContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| ScriptPromise PaymentRequest::Complete(ScriptState* script_state, |
| PaymentComplete result) { |
| if (!script_state->ContextIsValid()) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create(kInvalidStateError, "Cannot complete payment")); |
| } |
| |
| if (complete_resolver_) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, DOMException::Create(kInvalidStateError, |
| "Already called complete() once")); |
| } |
| |
| if (!complete_timer_.IsActive()) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, |
| DOMException::Create( |
| kInvalidStateError, |
| "Timed out after 60 seconds, complete() called too late")); |
| } |
| |
| // User has cancelled the transaction while the website was processing it. |
| if (!payment_provider_) { |
| return ScriptPromise::RejectWithDOMException( |
| script_state, DOMException::Create(kAbortError, "Request cancelled")); |
| } |
| |
| complete_timer_.Stop(); |
| |
| // The payment provider should respond in PaymentRequest::OnComplete(). |
| payment_provider_->Complete(payments::mojom::blink::PaymentComplete(result)); |
| |
| complete_resolver_ = ScriptPromiseResolver::Create(script_state); |
| return complete_resolver_->Promise(); |
| } |
| |
| void PaymentRequest::OnUpdatePaymentDetails( |
| const ScriptValue& details_script_value) { |
| if (!show_resolver_ || !payment_provider_) |
| return; |
| |
| PaymentDetailsUpdate details; |
| ExceptionState exception_state(v8::Isolate::GetCurrent(), |
| ExceptionState::kConstructionContext, |
| "PaymentDetailsUpdate"); |
| V8PaymentDetailsUpdate::toImpl(details_script_value.GetIsolate(), |
| details_script_value.V8Value(), details, |
| exception_state); |
| if (exception_state.HadException()) { |
| show_resolver_->Reject( |
| DOMException::Create(kSyntaxError, exception_state.Message())); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| PaymentDetailsPtr validated_details = |
| payments::mojom::blink::PaymentDetails::New(); |
| ValidateAndConvertPaymentDetailsUpdate( |
| details, validated_details, shipping_option_, *GetExecutionContext(), |
| exception_state); |
| if (exception_state.HadException()) { |
| show_resolver_->Reject( |
| DOMException::Create(kSyntaxError, exception_state.Message())); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| if (!options_.requestShipping()) |
| validated_details->shipping_options.clear(); |
| |
| payment_provider_->UpdateWith(std::move(validated_details)); |
| } |
| |
| void PaymentRequest::OnUpdatePaymentDetailsFailure(const String& error) { |
| if (show_resolver_) |
| show_resolver_->Reject(DOMException::Create(kAbortError, error)); |
| if (complete_resolver_) |
| complete_resolver_->Reject(DOMException::Create(kAbortError, error)); |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| DEFINE_TRACE(PaymentRequest) { |
| visitor->Trace(options_); |
| visitor->Trace(shipping_address_); |
| visitor->Trace(show_resolver_); |
| visitor->Trace(complete_resolver_); |
| visitor->Trace(abort_resolver_); |
| visitor->Trace(can_make_payment_resolver_); |
| EventTargetWithInlineData::Trace(visitor); |
| ContextLifecycleObserver::Trace(visitor); |
| } |
| |
| void PaymentRequest::OnCompleteTimeoutForTesting() { |
| complete_timer_.Stop(); |
| OnCompleteTimeout(0); |
| } |
| |
| PaymentRequest::PaymentRequest(ExecutionContext* execution_context, |
| const HeapVector<PaymentMethodData>& method_data, |
| const PaymentDetailsInit& details, |
| const PaymentOptions& options, |
| ExceptionState& exception_state) |
| : ContextLifecycleObserver(execution_context), |
| options_(options), |
| client_binding_(this), |
| complete_timer_( |
| TaskRunnerHelper::Get(TaskType::kMiscPlatformAPI, GetFrame()), |
| this, |
| &PaymentRequest::OnCompleteTimeout) { |
| if (!GetExecutionContext()->IsSecureContext()) { |
| exception_state.ThrowSecurityError("Must be in a secure context"); |
| return; |
| } |
| |
| if (!AllowedToUsePaymentRequest(GetFrame())) { |
| exception_state.ThrowSecurityError( |
| "Must be in a top-level browsing context or an iframe needs to specify " |
| "'allowpaymentrequest' explicitly"); |
| return; |
| } |
| |
| PaymentDetailsPtr validated_details = |
| payments::mojom::blink::PaymentDetails::New(); |
| validated_details->id = id_ = |
| details.hasId() ? details.id() : CreateCanonicalUUIDString(); |
| |
| Vector<payments::mojom::blink::PaymentMethodDataPtr> validated_method_data; |
| ValidateAndConvertPaymentMethodData(method_data, validated_method_data, |
| *GetExecutionContext(), exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| ValidateAndConvertPaymentDetailsInit(details, validated_details, |
| shipping_option_, *GetExecutionContext(), |
| exception_state); |
| if (exception_state.HadException()) |
| return; |
| |
| if (options_.requestShipping()) |
| shipping_type_ = options_.shippingType(); |
| else |
| validated_details->shipping_options.clear(); |
| |
| DCHECK(shipping_type_.IsNull() || shipping_type_ == "shipping" || |
| shipping_type_ == "delivery" || shipping_type_ == "pickup"); |
| |
| GetFrame()->GetInterfaceProvider()->GetInterface( |
| mojo::MakeRequest(&payment_provider_)); |
| payment_provider_.set_connection_error_handler(ConvertToBaseCallback( |
| WTF::Bind(&PaymentRequest::OnError, WrapWeakPersistent(this), |
| PaymentErrorReason::UNKNOWN))); |
| payment_provider_->Init( |
| client_binding_.CreateInterfacePtrAndBind(), |
| std::move(validated_method_data), std::move(validated_details), |
| payments::mojom::blink::PaymentOptions::From(options_)); |
| } |
| |
| void PaymentRequest::ContextDestroyed(ExecutionContext*) { |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| void PaymentRequest::OnShippingAddressChange(PaymentAddressPtr address) { |
| DCHECK(show_resolver_); |
| DCHECK(!complete_resolver_); |
| |
| String error_message; |
| if (!PaymentsValidators::IsValidShippingAddress(address, &error_message)) { |
| show_resolver_->Reject(DOMException::Create(kSyntaxError, error_message)); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| shipping_address_ = new PaymentAddress(std::move(address)); |
| PaymentRequestUpdateEvent* event = PaymentRequestUpdateEvent::Create( |
| GetExecutionContext(), EventTypeNames::shippingaddresschange); |
| event->SetTarget(this); |
| event->SetPaymentDetailsUpdater(this); |
| bool success = GetExecutionContext()->GetEventQueue()->EnqueueEvent(event); |
| DCHECK(success); |
| ALLOW_UNUSED_LOCAL(success); |
| } |
| |
| void PaymentRequest::OnShippingOptionChange(const String& shipping_option_id) { |
| DCHECK(show_resolver_); |
| DCHECK(!complete_resolver_); |
| shipping_option_ = shipping_option_id; |
| PaymentRequestUpdateEvent* event = PaymentRequestUpdateEvent::Create( |
| GetExecutionContext(), EventTypeNames::shippingoptionchange); |
| event->SetTarget(this); |
| event->SetPaymentDetailsUpdater(this); |
| bool success = GetExecutionContext()->GetEventQueue()->EnqueueEvent(event); |
| DCHECK(success); |
| ALLOW_UNUSED_LOCAL(success); |
| } |
| |
| void PaymentRequest::OnPaymentResponse(PaymentResponsePtr response) { |
| DCHECK(show_resolver_); |
| DCHECK(!complete_resolver_); |
| DCHECK(!complete_timer_.IsActive()); |
| |
| if (options_.requestShipping()) { |
| if (!response->shipping_address || response->shipping_option.IsEmpty()) { |
| show_resolver_->Reject(DOMException::Create(kSyntaxError)); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| String error_message; |
| if (!PaymentsValidators::IsValidShippingAddress(response->shipping_address, |
| &error_message)) { |
| show_resolver_->Reject(DOMException::Create(kSyntaxError, error_message)); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| shipping_address_ = new PaymentAddress(response->shipping_address.Clone()); |
| shipping_option_ = response->shipping_option; |
| } else { |
| if (response->shipping_address || !response->shipping_option.IsNull()) { |
| show_resolver_->Reject(DOMException::Create(kSyntaxError)); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| } |
| |
| if ((options_.requestPayerName() && response->payer_name.IsEmpty()) || |
| (options_.requestPayerEmail() && response->payer_email.IsEmpty()) || |
| (options_.requestPayerPhone() && response->payer_phone.IsEmpty()) || |
| (!options_.requestPayerName() && !response->payer_name.IsNull()) || |
| (!options_.requestPayerEmail() && !response->payer_email.IsNull()) || |
| (!options_.requestPayerPhone() && !response->payer_phone.IsNull())) { |
| show_resolver_->Reject(DOMException::Create(kSyntaxError)); |
| ClearResolversAndCloseMojoConnection(); |
| return; |
| } |
| |
| complete_timer_.StartOneShot(kCompleteTimeoutSeconds, BLINK_FROM_HERE); |
| |
| show_resolver_->Resolve(new PaymentResponse(std::move(response), this, id_)); |
| |
| // Do not close the mojo connection here. The merchant website should call |
| // PaymentResponse::complete(String), which will be forwarded over the mojo |
| // connection to display a success or failure message to the user. |
| show_resolver_.Clear(); |
| } |
| |
| void PaymentRequest::OnError(PaymentErrorReason error) { |
| ExceptionCode ec = kUnknownError; |
| String message; |
| |
| switch (error) { |
| case PaymentErrorReason::USER_CANCEL: |
| ec = kAbortError; |
| message = "Request cancelled"; |
| break; |
| case PaymentErrorReason::NOT_SUPPORTED: |
| ec = kNotSupportedError; |
| message = "The payment method is not supported"; |
| break; |
| case PaymentErrorReason::UNKNOWN: |
| ec = kUnknownError; |
| message = "Request failed"; |
| break; |
| } |
| |
| DCHECK(!message.IsEmpty()); |
| |
| if (complete_resolver_) |
| complete_resolver_->Reject(DOMException::Create(ec, message)); |
| |
| if (show_resolver_) |
| show_resolver_->Reject(DOMException::Create(ec, message)); |
| |
| if (abort_resolver_) |
| abort_resolver_->Reject(DOMException::Create(ec, message)); |
| |
| if (can_make_payment_resolver_) |
| can_make_payment_resolver_->Reject(DOMException::Create(ec, message)); |
| |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| void PaymentRequest::OnComplete() { |
| DCHECK(complete_resolver_); |
| complete_resolver_->Resolve(); |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| void PaymentRequest::OnAbort(bool aborted_successfully) { |
| DCHECK(abort_resolver_); |
| DCHECK(show_resolver_); |
| |
| if (!aborted_successfully) { |
| abort_resolver_->Reject(DOMException::Create( |
| kInvalidStateError, "Unable to abort the payment")); |
| abort_resolver_.Clear(); |
| return; |
| } |
| |
| show_resolver_->Reject( |
| DOMException::Create(kAbortError, "The website has aborted the payment")); |
| abort_resolver_->Resolve(); |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| void PaymentRequest::OnCanMakePayment(CanMakePaymentQueryResult result) { |
| DCHECK(can_make_payment_resolver_); |
| |
| switch (result) { |
| case CanMakePaymentQueryResult::WARNING_CAN_MAKE_PAYMENT: |
| WarnIgnoringQueryQuotaForCanMakePayment(*GetExecutionContext()); |
| // Intentionally fall through. |
| case CanMakePaymentQueryResult::CAN_MAKE_PAYMENT: |
| can_make_payment_resolver_->Resolve(true); |
| break; |
| case CanMakePaymentQueryResult::WARNING_CANNOT_MAKE_PAYMENT: |
| WarnIgnoringQueryQuotaForCanMakePayment(*GetExecutionContext()); |
| // Intentionally fall through. |
| case CanMakePaymentQueryResult::CANNOT_MAKE_PAYMENT: |
| can_make_payment_resolver_->Resolve(false); |
| break; |
| case CanMakePaymentQueryResult::QUERY_QUOTA_EXCEEDED: |
| can_make_payment_resolver_->Reject(DOMException::Create( |
| kNotAllowedError, "Not allowed to check whether can make payment")); |
| break; |
| } |
| |
| can_make_payment_resolver_.Clear(); |
| } |
| |
| void PaymentRequest::WarnNoFavicon() { |
| GetExecutionContext()->AddConsoleMessage( |
| ConsoleMessage::Create(kJSMessageSource, kWarningMessageLevel, |
| "Favicon not found for PaymentRequest UI. User " |
| "may not recognize the website.")); |
| } |
| |
| void PaymentRequest::OnCompleteTimeout(TimerBase*) { |
| GetExecutionContext()->AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kErrorMessageLevel, |
| "Timed out waiting for a PaymentResponse.complete() call.")); |
| payment_provider_->Complete(payments::mojom::blink::PaymentComplete(kFail)); |
| ClearResolversAndCloseMojoConnection(); |
| } |
| |
| void PaymentRequest::ClearResolversAndCloseMojoConnection() { |
| complete_timer_.Stop(); |
| complete_resolver_.Clear(); |
| show_resolver_.Clear(); |
| abort_resolver_.Clear(); |
| can_make_payment_resolver_.Clear(); |
| if (client_binding_.is_bound()) |
| client_binding_.Close(); |
| payment_provider_.reset(); |
| } |
| |
| } // namespace blink |