Chromium Code Reviews| Index: third_party/WebKit/Source/modules/payments/PaymentRequest.cpp |
| diff --git a/third_party/WebKit/Source/modules/payments/PaymentRequest.cpp b/third_party/WebKit/Source/modules/payments/PaymentRequest.cpp |
| index fe3ab4642012884480a3a1fe8edd66b0e6bdd892..8b6f819d1746c9b18e37ca360f02c151611dfc01 100644 |
| --- a/third_party/WebKit/Source/modules/payments/PaymentRequest.cpp |
| +++ b/third_party/WebKit/Source/modules/payments/PaymentRequest.cpp |
| @@ -6,13 +6,140 @@ |
| #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 "public/platform/Platform.h" |
| +#include <utility> |
| + |
| +namespace mojo { |
| + |
| +template <> |
| +struct TypeConverter<mojom::wtf::CurrencyAmountPtr, blink::CurrencyAmount> { |
| + static mojom::wtf::CurrencyAmountPtr Convert(const blink::CurrencyAmount& input) |
| + { |
| + mojom::wtf::CurrencyAmountPtr output = mojom::wtf::CurrencyAmount::New(); |
| + output->currency_code = input.currencyCode(); |
| + output->value = input.value(); |
| + return output; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojom::wtf::PaymentItemPtr, blink::PaymentItem> { |
| + static mojom::wtf::PaymentItemPtr Convert(const blink::PaymentItem& input) |
| + { |
| + mojom::wtf::PaymentItemPtr output = mojom::wtf::PaymentItem::New(); |
| + output->id = input.id(); |
| + output->label = input.label(); |
| + output->amount = mojom::wtf::CurrencyAmount::From(input.amount()); |
| + return output; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojom::wtf::ShippingOptionPtr, blink::ShippingOption> { |
| + static mojom::wtf::ShippingOptionPtr Convert(const blink::ShippingOption& input) |
| + { |
| + mojom::wtf::ShippingOptionPtr output = mojom::wtf::ShippingOption::New(); |
| + output->id = input.id(); |
| + output->label = input.label(); |
| + output->amount = mojom::wtf::CurrencyAmount::From(input.amount()); |
| + return output; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojom::wtf::PaymentDetailsPtr, blink::PaymentDetails> { |
| + static mojom::wtf::PaymentDetailsPtr Convert(const blink::PaymentDetails& input) |
| + { |
| + mojom::wtf::PaymentDetailsPtr output = mojom::wtf::PaymentDetails::New(); |
| + output->items = mojo::WTFArray<mojom::wtf::PaymentItemPtr>::New(input.items().size()); |
| + for (size_t i = 0; i < input.items().size(); ++i) { |
| + output->items[i] = mojom::wtf::PaymentItem::From(input.items()[i]); |
| + } |
| + output->shipping_options = mojo::WTFArray<mojom::wtf::ShippingOptionPtr>::New(input.hasShippingOptions() ? input.shippingOptions().size() : 0); |
| + if (input.hasShippingOptions()) { |
| + for (size_t i = 0; i < input.shippingOptions().size(); ++i) { |
| + output->shipping_options[i] = mojom::wtf::ShippingOption::From(input.shippingOptions()[i]); |
|
esprehn
2016/03/25 23:40:11
if we're going to be writing a lot of these copy :
Marijn Kruisselbrink
2016/03/25 23:59:39
See also my earlier comment; mojo::Array has these
please use gerrit instead
2016/03/29 22:15:45
Created a helper.
|
| + } |
| + } |
| + return output; |
| + } |
| +}; |
| + |
| +template <> |
| +struct TypeConverter<mojom::wtf::PaymentOptionsPtr, blink::PaymentOptions> { |
| + static mojom::wtf::PaymentOptionsPtr Convert(const blink::PaymentOptions& input) |
| + { |
| + mojom::wtf::PaymentOptionsPtr output = mojom::wtf::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> |
| +bool areValidItems(HeapVector<T> items, ExceptionState& exceptionState) |
|
esprehn
2016/03/25 23:40:11
hmm what two things have the exact same fields bel
please use gerrit instead
2016/03/29 22:15:45
See the comment for this function: "ShippingOption
|
| +{ |
| + String errorMessage; |
| + for (const auto& item : items) { |
| + if (!item.hasId()) { |
| + exceptionState.throwTypeError("Item id required"); |
| + return false; |
| + } |
| + |
| + if (!item.hasLabel()) { |
| + exceptionState.throwTypeError("Item label required"); |
| + return false; |
| + } |
| + |
| + if (!item.hasAmount()) { |
| + exceptionState.throwTypeError("Currency amount required"); |
| + return false; |
| + } |
| + |
| + if (!item.amount().hasCurrencyCode()) { |
| + exceptionState.throwTypeError("Currency code required"); |
| + return false; |
| + } |
| + |
| + if (!item.amount().hasValue()) { |
| + exceptionState.throwTypeError("Currency value required"); |
| + return false; |
| + } |
| + |
| + if (!isValidCurrencyCodeFormat(item.amount().currencyCode(), &errorMessage)) { |
| + exceptionState.throwTypeError(errorMessage); |
| + return false; |
| + } |
| + |
| + if (!isValidAmountFormat(item.amount().value(), &errorMessage)) { |
| + exceptionState.throwTypeError(errorMessage); |
| + return false; |
| + } |
| + } |
| + |
| + return true; |
|
esprehn
2016/03/25 23:40:11
you actually don't need the boolean, you can just
Marijn Kruisselbrink
2016/03/25 23:59:39
of course you'd still need to return as soon as yo
please use gerrit instead
2016/03/29 22:15:45
Done. Note that "else if" statements are not suita
|
| +} |
| + |
| +} // namespace |
| // static |
| PaymentRequest* PaymentRequest::create(ScriptState* scriptState, const Vector<String>& supportedMethods, const PaymentDetails& details, ExceptionState& exceptionState) |
| @@ -38,11 +165,27 @@ PaymentRequest::~PaymentRequest() |
| ScriptPromise PaymentRequest::show(ScriptState* scriptState) |
| { |
| - return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError, "Not implemented.")); |
| + if (m_showResolver) |
| + return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(InvalidStateError, "Already called show() once")); |
| + |
| + DCHECK(!m_paymentProvider.is_bound()); |
| + blink::Platform::current()->connectToRemoteService(mojo::GetProxy(&m_paymentProvider)); |
| + // TODO(rouslan): Handle connection errors. |
|
esprehn
2016/03/25 23:40:11
What's the correct way to handle this? Often todo'
please use gerrit instead
2016/03/29 22:15:45
Handling it now. It was not very well documented,
|
| + 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() |
| +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 |
| @@ -55,11 +198,24 @@ ExecutionContext* PaymentRequest::getExecutionContext() const |
| return m_scriptState->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); |
| RefCountedGarbageCollectedEventTargetWithInlineData<PaymentRequest>::trace(visitor); |
| } |
| @@ -68,12 +224,125 @@ PaymentRequest::PaymentRequest(ScriptState* scriptState, const Vector<String>& s |
| , 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; |
| + } |
| + |
| + if (!areValidItems(details.items(), exceptionState)) |
| + return; |
| + |
| + if (details.hasShippingOptions() && !areValidItems(details.shippingOptions(), exceptionState)) |
| + return; |
| + |
| if (!data.isEmpty()) { |
| RefPtr<JSONValue> value = toJSONValue(data.context(), data.v8Value()); |
|
esprehn
2016/03/25 23:40:11
this only returns null if maxDepth is reached btw
rwlbuis
2016/03/29 18:26:43
And I think there is a problem with the way it is
please use gerrit instead
2016/03/29 22:15:45
Added a layout test and throwing on infinite json,
|
| - if (value && value->getType() == JSONValue::TypeObject) |
| - m_stringifiedData = JSONObject::cast(value)->toJSONString(); |
| + if (value && !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"); |
|
esprehn
2016/03/25 23:40:11
hmm, if we have real requirements on the |data| wh
Marijn Kruisselbrink
2016/03/25 23:59:40
a WebIDL dictionary has a fixed, hardcoded set of
please use gerrit instead
2016/03/29 22:15:45
Correct.
|
| + return; |
| + } |
| + if (!paymentMethodSpecificKeyValue.value || paymentMethodSpecificKeyValue.value->isNull() || paymentMethodSpecificKeyValue.value->getType() != JSONValue::TypeObject) { |
|
esprehn
2016/03/25 23:40:11
ditto, all these null checks are kind of suspect.
rwlbuis
2016/03/29 19:30:46
Independent of this, note that getType() != TypeOb
please use gerrit instead
2016/03/29 22:15:45
Done.
please use gerrit instead
2016/03/29 22:15:45
Removed the "!paymentMethodSpecificKeyValue.value"
|
| + exceptionState.throwTypeError("Data for '" + paymentMethodSpecificKeyValue.key + "' should be a JSON-serializable object"); |
| + return; |
| + } |
| + } |
| + |
| + m_stringifiedData = jsonData->toJSONString(); |
|
esprehn
2016/03/25 23:40:11
why can't we reuse the data? This code is doing st
Marijn Kruisselbrink
2016/03/25 23:59:40
Where is it doing that? It starts as ScriptValue,
please use gerrit instead
2016/03/29 22:15:45
Acknowledged.
|
| + } |
| + } |
| + |
| + if (details.hasShippingOptions() && details.shippingOptions().size() == 1) |
|
esprehn
2016/03/25 23:40:11
what happens if you specified more than one? It se
Marijn Kruisselbrink
2016/03/25 23:59:40
I was confused here for a bit too, all the options
please use gerrit instead
2016/03/29 22:15:45
If multiple options are available, the user has to
|
| + m_shippingOption = details.shippingOptions()[0].id(); |
| +} |
| + |
| +void PaymentRequest::OnShippingAddressChange(mojom::wtf::ShippingAddressPtr address) |
| +{ |
| + DCHECK(m_showResolver); |
| + DCHECK(!m_completeResolver); |
| + |
| + // TODO(rouslan): Should the merchant website be notified of invalid shipping address |
| + // from the browser or the payment app? |
| + String errorMessage; |
| + if (!isValidRegionCodeFormat(address->region_code, &errorMessage) |
| + || !isValidLanguageCodeFormat(address->language_code, &errorMessage) |
| + || !isValidScriptCodeFormat(address->script_code, &errorMessage)) { |
| + m_showResolver->reject(DOMException::create(SyntaxError, errorMessage)); |
| + 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")); |
| + return; |
| + } |
| + |
| + m_shippingAddress = new ShippingAddress(std::move(address)); |
| + RefPtrWillBeRawPtr<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; |
| + RefPtrWillBeRawPtr<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")); |
| + return; |
| + } |
| + DCHECK(m_showResolver); |
| + m_showResolver->reject(DOMException::create(SyntaxError, "Request cancelled")); |
| + m_clientBinding.Close(); |
| + m_paymentProvider.reset(); |
| +} |
| + |
| +void PaymentRequest::OnComplete() |
| +{ |
| + DCHECK(m_completeResolver); |
| + m_completeResolver->resolve(); |
| + m_clientBinding.Close(); |
| + m_paymentProvider.reset(); |
| } |
| } // namespace blink |