| // 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 "bindings/core/v8/ExceptionState.h" |
| #include "bindings/core/v8/JSONValuesForV8.h" |
| #include "bindings/core/v8/ScriptPromiseResolver.h" |
| #include "bindings/core/v8/ScriptState.h" |
| #include "core/EventTypeNames.h" |
| #include "core/dom/DOMException.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "core/events/Event.h" |
| #include "core/events/EventQueue.h" |
| #include "modules/EventTargetModulesNames.h" |
| #include "modules/payments/PaymentItem.h" |
| #include "modules/payments/PaymentResponse.h" |
| #include "modules/payments/PaymentsValidators.h" |
| #include "modules/payments/ShippingAddress.h" |
| #include "modules/payments/ShippingOption.h" |
| #include "mojo/public/cpp/bindings/interface_request.h" |
| #include "mojo/public/cpp/bindings/wtf_array.h" |
| #include "platform/MojoHelper.h" |
| #include "public/platform/ServiceRegistry.h" |
| #include <utility> |
| |
| namespace mojo { |
| |
| using blink::mojom::wtf::CurrencyAmount; |
| using blink::mojom::wtf::CurrencyAmountPtr; |
| using blink::mojom::wtf::PaymentDetails; |
| using blink::mojom::wtf::PaymentDetailsPtr; |
| using blink::mojom::wtf::PaymentItem; |
| using blink::mojom::wtf::PaymentItemPtr; |
| using blink::mojom::wtf::PaymentOptions; |
| using blink::mojom::wtf::PaymentOptionsPtr; |
| using blink::mojom::wtf::ShippingOption; |
| using blink::mojom::wtf::ShippingOptionPtr; |
| |
| template <> |
| struct TypeConverter<CurrencyAmountPtr, blink::CurrencyAmount> { |
| static CurrencyAmountPtr Convert(const blink::CurrencyAmount& input) |
| { |
| CurrencyAmountPtr output = CurrencyAmount::New(); |
| output->currency_code = input.currencyCode(); |
| output->value = input.value(); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentItemPtr, blink::PaymentItem> { |
| static PaymentItemPtr Convert(const blink::PaymentItem& input) |
| { |
| PaymentItemPtr output = PaymentItem::New(); |
| output->id = input.id(); |
| output->label = input.label(); |
| output->amount = CurrencyAmount::From(input.amount()); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<ShippingOptionPtr, blink::ShippingOption> { |
| static ShippingOptionPtr Convert(const blink::ShippingOption& input) |
| { |
| ShippingOptionPtr output = ShippingOption::New(); |
| output->id = input.id(); |
| output->label = input.label(); |
| output->amount = CurrencyAmount::From(input.amount()); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentDetailsPtr, blink::PaymentDetails> { |
| static PaymentDetailsPtr Convert(const blink::PaymentDetails& input) |
| { |
| PaymentDetailsPtr output = PaymentDetails::New(); |
| output->items = mojo::WTFArray<PaymentItemPtr>::From(input.items()); |
| if (input.hasShippingOptions()) |
| output->shipping_options = mojo::WTFArray<ShippingOptionPtr>::From(input.shippingOptions()); |
| else |
| output->shipping_options = mojo::WTFArray<ShippingOptionPtr>::New(0); |
| return output; |
| } |
| }; |
| |
| template <> |
| struct TypeConverter<PaymentOptionsPtr, blink::PaymentOptions> { |
| static PaymentOptionsPtr Convert(const blink::PaymentOptions& input) |
| { |
| PaymentOptionsPtr output = PaymentOptions::New(); |
| output->request_shipping = input.requestShipping(); |
| return output; |
| } |
| }; |
| |
| } // namespace mojo |
| |
| namespace blink { |
| namespace { |
| |
| // Validates ShippingOption and PaymentItem dictionaries, which happen to have identical fields. |
| template <typename T> |
| void validateShippingOptionsOrPaymentItems(HeapVector<T> items, ExceptionState& exceptionState) |
| { |
| String errorMessage; |
| for (const auto& item : items) { |
| if (!item.hasId()) { |
| exceptionState.throwTypeError("Item id required"); |
| return; |
| } |
| |
| if (!item.hasLabel()) { |
| exceptionState.throwTypeError("Item label required"); |
| return; |
| } |
| |
| if (!item.hasAmount()) { |
| exceptionState.throwTypeError("Currency amount required"); |
| return; |
| } |
| |
| if (!item.amount().hasCurrencyCode()) { |
| exceptionState.throwTypeError("Currency code required"); |
| return; |
| } |
| |
| if (!item.amount().hasValue()) { |
| exceptionState.throwTypeError("Currency value required"); |
| return; |
| } |
| |
| if (!PaymentsValidators::isValidCurrencyCodeFormat(item.amount().currencyCode(), &errorMessage)) { |
| exceptionState.throwTypeError(errorMessage); |
| return; |
| } |
| |
| if (!PaymentsValidators::isValidAmountFormat(item.amount().value(), &errorMessage)) { |
| exceptionState.throwTypeError(errorMessage); |
| return; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| PaymentRequest* PaymentRequest::create(ScriptState* scriptState, const Vector<String>& supportedMethods, const PaymentDetails& details, ExceptionState& exceptionState) |
| { |
| return new PaymentRequest(scriptState, supportedMethods, details, PaymentOptions(), ScriptValue(), exceptionState); |
| } |
| |
| PaymentRequest* PaymentRequest::create(ScriptState* scriptState, const Vector<String>& supportedMethods, const PaymentDetails& details, const PaymentOptions& options, ExceptionState& exceptionState) |
| { |
| return new PaymentRequest(scriptState, supportedMethods, details, options, ScriptValue(), exceptionState); |
| } |
| |
| PaymentRequest* PaymentRequest::create(ScriptState* scriptState, const Vector<String>& supportedMethods, const PaymentDetails& details, const PaymentOptions& options, const ScriptValue& data, ExceptionState& exceptionState) |
| { |
| return new PaymentRequest(scriptState, supportedMethods, details, options, data, exceptionState); |
| } |
| |
| PaymentRequest::~PaymentRequest() |
| { |
| } |
| |
| ScriptPromise PaymentRequest::show(ScriptState* scriptState) |
| { |
| if (m_showResolver) |
| return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(InvalidStateError, "Already called show() once")); |
| |
| if (!scriptState->domWindow() || !scriptState->domWindow()->frame()) |
| return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(InvalidStateError, "Cannot show the payment request")); |
| |
| DCHECK(!m_paymentProvider.is_bound()); |
| scriptState->domWindow()->frame()->serviceRegistry()->connectToRemoteService(mojo::GetProxy(&m_paymentProvider)); |
| m_paymentProvider.set_connection_error_handler(sameThreadBindForMojo(&PaymentRequest::OnError, this)); |
| m_paymentProvider->SetClient(m_clientBinding.CreateInterfacePtrAndBind()); |
| m_paymentProvider->Show(std::move(m_supportedMethods), mojom::wtf::PaymentDetails::From(m_details), mojom::wtf::PaymentOptions::From(m_options), m_stringifiedData.isNull() ? "" : m_stringifiedData); |
| |
| m_showResolver = ScriptPromiseResolver::create(scriptState); |
| return m_showResolver->promise(); |
| } |
| |
| void PaymentRequest::abort(ExceptionState& exceptionState) |
| { |
| if (!m_showResolver) { |
| exceptionState.throwDOMException(InvalidStateError, "Never called show(), so nothing to abort"); |
| return; |
| } |
| |
| m_paymentProvider->Abort(); |
| } |
| |
| const AtomicString& PaymentRequest::interfaceName() const |
| { |
| return EventTargetNames::PaymentRequest; |
| } |
| |
| ExecutionContext* PaymentRequest::getExecutionContext() const |
| { |
| return ContextLifecycleObserver::getExecutionContext(); |
| } |
| |
| ScriptPromise PaymentRequest::complete(ScriptState* scriptState, bool success) |
| { |
| if (m_completeResolver) |
| return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(InvalidStateError, "Already called complete() once")); |
| |
| m_completeResolver = ScriptPromiseResolver::create(scriptState); |
| m_paymentProvider->Complete(success); |
| |
| return m_completeResolver->promise(); |
| } |
| |
| DEFINE_TRACE(PaymentRequest) |
| { |
| visitor->trace(m_details); |
| visitor->trace(m_options); |
| visitor->trace(m_shippingAddress); |
| visitor->trace(m_showResolver); |
| visitor->trace(m_completeResolver); |
| EventTargetWithInlineData::trace(visitor); |
| ContextLifecycleObserver::trace(visitor); |
| } |
| |
| PaymentRequest::PaymentRequest(ScriptState* scriptState, const Vector<String>& supportedMethods, const PaymentDetails& details, const PaymentOptions& options, const ScriptValue& data, ExceptionState& exceptionState) |
| : ContextLifecycleObserver(scriptState->getExecutionContext()) |
| , m_supportedMethods(supportedMethods) |
| , m_details(details) |
| , m_options(options) |
| , m_clientBinding(this) |
| { |
| // TODO(rouslan): Also check for a top-level browsing context. |
| // https://github.com/w3c/browser-payment-api/issues/2 |
| if (!scriptState->getExecutionContext()->isSecureContext()) { |
| exceptionState.throwSecurityError("Must be in a secure context"); |
| return; |
| } |
| |
| if (supportedMethods.isEmpty()) { |
| exceptionState.throwTypeError("Must specify at least one payment method identifier"); |
| return; |
| } |
| |
| if (!details.hasItems()) { |
| exceptionState.throwTypeError("Must specify items"); |
| return; |
| } |
| |
| if (details.items().isEmpty()) { |
| exceptionState.throwTypeError("Must specify at least one item"); |
| return; |
| } |
| |
| validateShippingOptionsOrPaymentItems(details.items(), exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| |
| if (details.hasShippingOptions()) { |
| validateShippingOptionsOrPaymentItems(details.shippingOptions(), exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| } |
| |
| if (!data.isEmpty()) { |
| RefPtr<JSONValue> value = toJSONValue(data.context(), data.v8Value()); |
| if (!value) { |
| exceptionState.throwTypeError("Unable to parse payment method specific data"); |
| return; |
| } |
| if (!value->isNull()) { |
| if (value->getType() != JSONValue::TypeObject) { |
| exceptionState.throwTypeError("Payment method specific data should be a JSON-serializable object"); |
| return; |
| } |
| |
| RefPtr<JSONObject> jsonData = JSONObject::cast(value); |
| for (const auto& paymentMethodSpecificKeyValue : *jsonData) { |
| if (!supportedMethods.contains(paymentMethodSpecificKeyValue.key)) { |
| exceptionState.throwTypeError("'" + paymentMethodSpecificKeyValue.key + "' should match one of the payment method identifiers"); |
| return; |
| } |
| if (paymentMethodSpecificKeyValue.value->getType() != JSONValue::TypeObject) { |
| exceptionState.throwTypeError("Data for '" + paymentMethodSpecificKeyValue.key + "' should be a JSON-serializable object"); |
| return; |
| } |
| } |
| |
| m_stringifiedData = jsonData->toJSONString(); |
| } |
| } |
| |
| // Set the currently selected option if only one option was passed. |
| if (details.hasShippingOptions() && details.shippingOptions().size() == 1) |
| m_shippingOption = details.shippingOptions().begin()->id(); |
| } |
| |
| void PaymentRequest::contextDestroyed() |
| { |
| cleanUp(); |
| } |
| |
| void PaymentRequest::OnShippingAddressChange(mojom::wtf::ShippingAddressPtr address) |
| { |
| DCHECK(m_showResolver); |
| DCHECK(!m_completeResolver); |
| |
| String errorMessage; |
| if (!PaymentsValidators::isValidRegionCodeFormat(address->region_code, &errorMessage) |
| || !PaymentsValidators::isValidLanguageCodeFormat(address->language_code, &errorMessage) |
| || !PaymentsValidators::isValidScriptCodeFormat(address->script_code, &errorMessage)) { |
| m_showResolver->reject(DOMException::create(SyntaxError, errorMessage)); |
| cleanUp(); |
| return; |
| } |
| |
| if (address->language_code.isEmpty() && !address->script_code.isEmpty()) { |
| m_showResolver->reject(DOMException::create(SyntaxError, "If language code is empty, then script code should also be empty")); |
| cleanUp(); |
| return; |
| } |
| |
| m_shippingAddress = new ShippingAddress(std::move(address)); |
| Event* event = Event::create(EventTypeNames::shippingaddresschange); |
| event->setTarget(this); |
| getExecutionContext()->getEventQueue()->enqueueEvent(event); |
| } |
| |
| void PaymentRequest::OnShippingOptionChange(const String& shippingOptionId) |
| { |
| DCHECK(m_showResolver); |
| DCHECK(!m_completeResolver); |
| m_shippingOption = shippingOptionId; |
| Event* event = Event::create(EventTypeNames::shippingoptionchange); |
| event->setTarget(this); |
| getExecutionContext()->getEventQueue()->enqueueEvent(event); |
| } |
| |
| void PaymentRequest::OnPaymentResponse(mojom::wtf::PaymentResponsePtr response) |
| { |
| DCHECK(m_showResolver); |
| DCHECK(!m_completeResolver); |
| m_showResolver->resolve(new PaymentResponse(std::move(response), this)); |
| } |
| |
| void PaymentRequest::OnError() |
| { |
| if (m_completeResolver) |
| m_completeResolver->reject(DOMException::create(SyntaxError, "Request cancelled")); |
| if (m_showResolver) |
| m_showResolver->reject(DOMException::create(SyntaxError, "Request cancelled")); |
| cleanUp(); |
| } |
| |
| void PaymentRequest::OnComplete() |
| { |
| DCHECK(m_completeResolver); |
| m_completeResolver->resolve(); |
| cleanUp(); |
| } |
| |
| void PaymentRequest::cleanUp() |
| { |
| m_completeResolver.clear(); |
| m_showResolver.clear(); |
| if (m_clientBinding.is_bound()) |
| m_clientBinding.Close(); |
| m_paymentProvider.reset(); |
| } |
| |
| } // namespace blink |