2 * Copyright (C) 2017-2018 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
27 #include "PaymentRequest.h"
29 #if ENABLE(PAYMENT_REQUEST)
31 #include "ApplePayPaymentHandler.h"
33 #include "EventNames.h"
34 #include "JSDOMPromise.h"
35 #include "JSPaymentDetailsUpdate.h"
36 #include "JSPaymentResponse.h"
37 #include "PaymentAddress.h"
38 #include "PaymentCurrencyAmount.h"
39 #include "PaymentDetailsInit.h"
40 #include "PaymentHandler.h"
41 #include "PaymentMethodData.h"
42 #include "PaymentOptions.h"
43 #include "PaymentRequestUpdateEvent.h"
44 #include "PaymentResponse.h"
45 #include "ScriptController.h"
46 #include <JavaScriptCore/JSONObject.h>
47 #include <JavaScriptCore/ThrowScope.h>
48 #include <wtf/ASCIICType.h>
49 #include <wtf/RunLoop.h>
50 #include <wtf/Scope.h>
55 // Implements the IsWellFormedCurrencyCode abstract operation from ECMA 402
56 // https://tc39.github.io/ecma402/#sec-iswellformedcurrencycode
57 static bool isWellFormedCurrencyCode(const String& currency)
59 if (currency.length() == 3)
60 return currency.isAllSpecialCharacters<isASCIIAlpha>();
64 // Implements the "valid decimal monetary value" validity checker
65 // https://www.w3.org/TR/payment-request/#dfn-valid-decimal-monetary-value
66 static bool isValidDecimalMonetaryValue(StringView value)
76 auto state = State::Start;
77 for (auto character : value.codeUnits()) {
80 if (character == '-') {
85 if (isASCIIDigit(character)) {
93 if (isASCIIDigit(character)) {
101 if (character == '.') {
106 if (isASCIIDigit(character)) {
107 state = State::Digit;
114 if (isASCIIDigit(character)) {
115 state = State::DotDigit;
121 case State::DotDigit:
122 if (isASCIIDigit(character)) {
123 state = State::DotDigit;
131 if (state == State::Digit || state == State::DotDigit)
137 // Implements the "check and canonicalize amount" validity checker
138 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-amount
139 static ExceptionOr<void> checkAndCanonicalizeAmount(PaymentCurrencyAmount& amount)
141 if (amount.currencySystem != "urn:iso:std:iso:4217")
144 if (!isWellFormedCurrencyCode(amount.currency))
145 return Exception { RangeError, makeString("\"", amount.currency, "\" is not a valid currency code.") };
147 if (!isValidDecimalMonetaryValue(amount.value))
148 return Exception { TypeError, makeString("\"", amount.value, "\" is not a valid decimal monetary value.") };
150 amount.currency = amount.currency.convertToASCIIUppercase();
154 // Implements the "check and canonicalize total" validity checker
155 // https://www.w3.org/TR/payment-request/#dfn-check-and-canonicalize-total
156 static ExceptionOr<void> checkAndCanonicalizeTotal(PaymentCurrencyAmount& total)
158 if (total.currencySystem != "urn:iso:std:iso:4217")
161 auto exception = checkAndCanonicalizeAmount(total);
162 if (exception.hasException())
165 if (total.value[0] == '-')
166 return Exception { TypeError, ASCIILiteral("Total currency values cannot be negative.") };
171 // Implements "validate a standardized payment method identifier"
172 // https://www.w3.org/TR/payment-method-id/#validity-0
173 static bool isValidStandardizedPaymentMethodIdentifier(StringView identifier)
182 auto state = State::Start;
183 for (auto character : identifier.codeUnits()) {
187 if (isASCIILower(character)) {
188 state = State::LowerAlpha;
194 case State::LowerAlpha:
196 if (isASCIILower(character)) {
197 state = State::LowerAlpha;
201 if (isASCIIDigit(character)) {
202 state = State::Digit;
206 if (character == '-') {
207 state = State::Hyphen;
215 return state == State::LowerAlpha || state == State::Digit;
218 // Implements "validate a URL-based payment method identifier"
219 // https://www.w3.org/TR/payment-method-id/#validation
220 static bool isValidURLBasedPaymentMethodIdentifier(const URL& url)
222 if (!url.protocolIs("https"))
225 if (!url.user().isEmpty() || !url.pass().isEmpty())
231 // Implements "validate a payment method identifier"
232 // https://www.w3.org/TR/payment-method-id/#validity
233 std::optional<PaymentRequest::MethodIdentifier> convertAndValidatePaymentMethodIdentifier(const String& identifier)
235 URL url { URL(), identifier };
236 if (!url.isValid()) {
237 if (isValidStandardizedPaymentMethodIdentifier(identifier))
238 return { identifier };
242 if (isValidURLBasedPaymentMethodIdentifier(url))
243 return { WTFMove(url) };
248 enum class ShouldValidatePaymentMethodIdentifier {
253 static ExceptionOr<std::tuple<String, Vector<String>>> checkAndCanonicalizeDetails(JSC::ExecState& execState, PaymentDetailsBase& details, bool requestShipping, ShouldValidatePaymentMethodIdentifier shouldValidatePaymentMethodIdentifier)
255 for (auto& item : details.displayItems) {
256 auto exception = checkAndCanonicalizeAmount(item.amount);
257 if (exception.hasException())
258 return exception.releaseException();
261 String selectedShippingOption;
262 if (requestShipping) {
263 HashSet<String> seenShippingOptionIDs;
264 for (auto& shippingOption : details.shippingOptions) {
265 auto exception = checkAndCanonicalizeAmount(shippingOption.amount);
266 if (exception.hasException())
267 return exception.releaseException();
269 auto addResult = seenShippingOptionIDs.add(shippingOption.id);
270 if (!addResult.isNewEntry)
271 return Exception { TypeError, "Shipping option IDs must be unique." };
273 if (shippingOption.selected)
274 selectedShippingOption = shippingOption.id;
278 Vector<String> serializedModifierData;
279 serializedModifierData.reserveInitialCapacity(details.modifiers.size());
280 for (auto& modifier : details.modifiers) {
281 if (shouldValidatePaymentMethodIdentifier == ShouldValidatePaymentMethodIdentifier::Yes) {
282 auto paymentMethodIdentifier = convertAndValidatePaymentMethodIdentifier(modifier.supportedMethods);
283 if (!paymentMethodIdentifier)
284 return Exception { RangeError, makeString("\"", modifier.supportedMethods, "\" is an invalid payment method identifier.") };
287 if (modifier.total) {
288 auto exception = checkAndCanonicalizeTotal(modifier.total->amount);
289 if (exception.hasException())
290 return exception.releaseException();
293 for (auto& item : modifier.additionalDisplayItems) {
294 auto exception = checkAndCanonicalizeAmount(item.amount);
295 if (exception.hasException())
296 return exception.releaseException();
299 String serializedData;
301 auto scope = DECLARE_THROW_SCOPE(execState.vm());
302 serializedData = JSONStringify(&execState, modifier.data.get(), 0);
303 if (scope.exception())
304 return Exception { ExistingExceptionError };
305 modifier.data.clear();
307 serializedModifierData.uncheckedAppend(WTFMove(serializedData));
310 return std::make_tuple(WTFMove(selectedShippingOption), WTFMove(serializedModifierData));
313 // Implements the PaymentRequest Constructor
314 // https://www.w3.org/TR/payment-request/#constructor
315 ExceptionOr<Ref<PaymentRequest>> PaymentRequest::create(Document& document, Vector<PaymentMethodData>&& methodData, PaymentDetailsInit&& details, PaymentOptions&& options)
317 auto canCreateSession = PaymentHandler::canCreateSession(document);
318 if (canCreateSession.hasException())
319 return canCreateSession.releaseException();
321 if (details.id.isNull())
322 details.id = createCanonicalUUIDString();
324 if (methodData.isEmpty())
325 return Exception { TypeError, ASCIILiteral("At least one payment method is required.") };
327 Vector<Method> serializedMethodData;
328 serializedMethodData.reserveInitialCapacity(methodData.size());
329 for (auto& paymentMethod : methodData) {
330 auto identifier = convertAndValidatePaymentMethodIdentifier(paymentMethod.supportedMethods);
332 return Exception { RangeError, makeString("\"", paymentMethod.supportedMethods, "\" is an invalid payment method identifier.") };
334 String serializedData;
335 if (paymentMethod.data) {
336 auto scope = DECLARE_THROW_SCOPE(document.execState()->vm());
337 serializedData = JSONStringify(document.execState(), paymentMethod.data.get(), 0);
338 if (scope.exception())
339 return Exception { ExistingExceptionError };
341 serializedMethodData.uncheckedAppend({ WTFMove(*identifier), WTFMove(serializedData) });
344 auto totalResult = checkAndCanonicalizeTotal(details.total.amount);
345 if (totalResult.hasException())
346 return totalResult.releaseException();
348 auto detailsResult = checkAndCanonicalizeDetails(*document.execState(), details, options.requestShipping, ShouldValidatePaymentMethodIdentifier::No);
349 if (detailsResult.hasException())
350 return detailsResult.releaseException();
352 auto shippingOptionAndModifierData = detailsResult.releaseReturnValue();
353 return adoptRef(*new PaymentRequest(document, WTFMove(options), WTFMove(details), WTFMove(std::get<1>(shippingOptionAndModifierData)), WTFMove(serializedMethodData), WTFMove(std::get<0>(shippingOptionAndModifierData))));
356 PaymentRequest::PaymentRequest(Document& document, PaymentOptions&& options, PaymentDetailsInit&& details, Vector<String>&& serializedModifierData, Vector<Method>&& serializedMethodData, String&& selectedShippingOption)
357 : ActiveDOMObject { &document }
358 , m_options { WTFMove(options) }
359 , m_details { WTFMove(details) }
360 , m_serializedModifierData { WTFMove(serializedModifierData) }
361 , m_serializedMethodData { WTFMove(serializedMethodData) }
362 , m_shippingOption { WTFMove(selectedShippingOption) }
367 PaymentRequest::~PaymentRequest()
369 ASSERT(!hasPendingActivity());
370 ASSERT(!m_activePaymentHandler);
373 static ExceptionOr<JSC::JSValue> parse(ScriptExecutionContext& context, const String& string)
375 auto scope = DECLARE_THROW_SCOPE(context.vm());
376 JSC::JSValue data = JSONParse(context.execState(), string);
377 if (scope.exception())
378 return Exception { ExistingExceptionError };
379 return WTFMove(data);
382 // https://www.w3.org/TR/payment-request/#show()-method
383 void PaymentRequest::show(Document& document, RefPtr<DOMPromise>&& detailsPromise, ShowPromise&& promise)
385 if (!document.frame()) {
386 promise.reject(Exception { AbortError });
390 if (!UserGestureIndicator::processingUserGesture()) {
391 promise.reject(Exception { SecurityError, "show() must be triggered by user activation." });
395 if (m_state != State::Created) {
396 promise.reject(Exception { InvalidStateError });
400 if (PaymentHandler::hasActiveSession(document)) {
401 promise.reject(Exception { AbortError });
405 m_state = State::Interactive;
406 ASSERT(!m_showPromise);
407 m_showPromise = WTFMove(promise);
409 RefPtr<PaymentHandler> selectedPaymentHandler;
410 for (auto& paymentMethod : m_serializedMethodData) {
411 auto data = parse(document, paymentMethod.serializedData);
412 if (data.hasException()) {
413 m_showPromise->reject(data.releaseException());
417 auto handler = PaymentHandler::create(document, *this, paymentMethod.identifier);
421 auto result = handler->convertData(data.releaseReturnValue());
422 if (result.hasException()) {
423 m_showPromise->reject(result.releaseException());
427 if (!selectedPaymentHandler)
428 selectedPaymentHandler = WTFMove(handler);
431 if (!selectedPaymentHandler) {
432 m_showPromise->reject(Exception { NotSupportedError });
436 auto exception = selectedPaymentHandler->show();
437 if (exception.hasException()) {
438 m_showPromise->reject(exception.releaseException());
442 ASSERT(!m_activePaymentHandler);
443 m_activePaymentHandler = WTFMove(selectedPaymentHandler);
444 setPendingActivity(this); // unsetPendingActivity() is called below in stop()
449 exception = updateWith(UpdateReason::ShowDetailsResolved, detailsPromise.releaseNonNull());
450 ASSERT(!exception.hasException());
453 void PaymentRequest::abortWithException(Exception&& exception)
455 if (m_state != State::Interactive)
458 if (auto paymentHandler = std::exchange(m_activePaymentHandler, nullptr)) {
459 unsetPendingActivity(this);
460 paymentHandler->hide();
463 ASSERT(m_state == State::Interactive);
464 m_state = State::Closed;
465 m_showPromise->reject(WTFMove(exception));
468 void PaymentRequest::stop()
470 abortWithException(Exception { AbortError });
473 // https://www.w3.org/TR/payment-request/#abort()-method
474 ExceptionOr<void> PaymentRequest::abort(AbortPromise&& promise)
476 if (m_state != State::Interactive)
477 return Exception { InvalidStateError };
484 // https://www.w3.org/TR/payment-request/#canmakepayment()-method
485 void PaymentRequest::canMakePayment(Document& document, CanMakePaymentPromise&& promise)
487 if (m_state != State::Created) {
488 promise.reject(Exception { InvalidStateError });
492 for (auto& paymentMethod : m_serializedMethodData) {
493 auto data = parse(document, paymentMethod.serializedData);
494 if (data.hasException())
497 auto handler = PaymentHandler::create(document, *this, paymentMethod.identifier);
501 auto exception = handler->convertData(data.releaseReturnValue());
502 if (exception.hasException())
505 handler->canMakePayment([promise = WTFMove(promise)](bool canMakePayment) mutable {
506 promise.resolve(canMakePayment);
511 promise.resolve(false);
514 const String& PaymentRequest::id() const
519 std::optional<PaymentShippingType> PaymentRequest::shippingType() const
521 if (m_options.requestShipping)
522 return m_options.shippingType;
526 bool PaymentRequest::canSuspendForDocumentSuspension() const
530 ASSERT(!m_activePaymentHandler);
532 case State::Interactive:
534 return !m_activePaymentHandler;
538 void PaymentRequest::shippingAddressChanged(Ref<PaymentAddress>&& shippingAddress)
540 whenDetailsSettled([this, protectedThis = makeRefPtr(this), shippingAddress = makeRefPtr(shippingAddress.get())]() mutable {
541 m_shippingAddress = WTFMove(shippingAddress);
542 dispatchEvent(PaymentRequestUpdateEvent::create(eventNames().shippingaddresschangeEvent, *this));
546 void PaymentRequest::shippingOptionChanged(const String& shippingOption)
548 whenDetailsSettled([this, protectedThis = makeRefPtr(this), shippingOption]() mutable {
549 m_shippingOption = shippingOption;
550 dispatchEvent(PaymentRequestUpdateEvent::create(eventNames().shippingoptionchangeEvent, *this));
554 void PaymentRequest::paymentMethodChanged()
556 whenDetailsSettled([this, protectedThis = makeRefPtr(this)] {
557 m_activePaymentHandler->detailsUpdated(UpdateReason::PaymentMethodChanged, { });
561 ExceptionOr<void> PaymentRequest::updateWith(UpdateReason reason, Ref<DOMPromise>&& promise)
563 if (m_state != State::Interactive)
564 return Exception { InvalidStateError };
567 return Exception { InvalidStateError };
571 ASSERT(!m_detailsPromise);
572 m_detailsPromise = WTFMove(promise);
573 m_detailsPromise->whenSettled([this, protectedThis = makeRefPtr(this), reason]() {
574 settleDetailsPromise(reason);
580 ExceptionOr<void> PaymentRequest::completeMerchantValidation(Event& event, Ref<DOMPromise>&& merchantSessionPromise)
582 if (m_state != State::Interactive)
583 return Exception { InvalidStateError };
585 event.stopPropagation();
586 event.stopImmediatePropagation();
588 m_merchantSessionPromise = WTFMove(merchantSessionPromise);
589 m_merchantSessionPromise->whenSettled([this, protectedThis = makeRefPtr(this)]() {
590 if (m_state != State::Interactive)
593 if (m_merchantSessionPromise->status() == DOMPromise::Status::Rejected) {
598 auto exception = m_activePaymentHandler->merchantValidationCompleted(m_merchantSessionPromise->result());
599 if (exception.hasException()) {
600 abortWithException(exception.releaseException());
608 void PaymentRequest::settleDetailsPromise(UpdateReason reason)
610 auto scopeExit = makeScopeExit([&] {
611 m_isUpdating = false;
612 m_detailsPromise = nullptr;
615 if (m_state != State::Interactive)
618 if (m_detailsPromise->status() == DOMPromise::Status::Rejected) {
623 auto& context = *m_detailsPromise->scriptExecutionContext();
624 auto throwScope = DECLARE_THROW_SCOPE(context.vm());
625 auto paymentDetailsUpdate = convertDictionary<PaymentDetailsUpdate>(*context.execState(), m_detailsPromise->result());
626 if (throwScope.exception()) {
627 abortWithException(Exception { ExistingExceptionError });
631 auto totalResult = checkAndCanonicalizeTotal(paymentDetailsUpdate.total.amount);
632 if (totalResult.hasException()) {
633 abortWithException(totalResult.releaseException());
637 auto detailsResult = checkAndCanonicalizeDetails(*context.execState(), paymentDetailsUpdate, m_options.requestShipping, ShouldValidatePaymentMethodIdentifier::Yes);
638 if (detailsResult.hasException()) {
639 abortWithException(detailsResult.releaseException());
643 auto shippingOptionAndModifierData = detailsResult.releaseReturnValue();
645 m_details.total = WTFMove(paymentDetailsUpdate.total);
646 m_details.displayItems = WTFMove(paymentDetailsUpdate.displayItems);
647 if (m_options.requestShipping) {
648 m_details.shippingOptions = WTFMove(paymentDetailsUpdate.shippingOptions);
649 m_shippingOption = WTFMove(std::get<0>(shippingOptionAndModifierData));
652 m_details.modifiers = WTFMove(paymentDetailsUpdate.modifiers);
653 m_serializedModifierData = WTFMove(std::get<1>(shippingOptionAndModifierData));
655 auto result = m_activePaymentHandler->detailsUpdated(reason, paymentDetailsUpdate.error);
656 if (result.hasException()) {
657 abortWithException(result.releaseException());
662 void PaymentRequest::whenDetailsSettled(std::function<void()>&& callback)
664 if (!m_detailsPromise) {
665 ASSERT(m_state == State::Interactive);
666 ASSERT(!m_isUpdating);
671 m_detailsPromise->whenSettled([this, protectedThis = makeRefPtr(this), callback = WTFMove(callback)] {
672 if (m_state != State::Interactive)
675 ASSERT(!m_isUpdating);
680 void PaymentRequest::accept(const String& methodName, JSC::Strong<JSC::JSObject>&& details, Ref<PaymentAddress>&& shippingAddress, const String& payerName, const String& payerEmail, const String& payerPhone)
682 ASSERT(m_state == State::Interactive);
684 auto response = PaymentResponse::create(*this);
685 response->setRequestId(m_details.id);
686 response->setMethodName(methodName);
687 response->setDetails(WTFMove(details));
689 if (m_options.requestShipping) {
690 response->setShippingAddress(shippingAddress.ptr());
691 response->setShippingOption(m_shippingOption);
694 if (m_options.requestPayerName)
695 response->setPayerName(payerName);
697 if (m_options.requestPayerEmail)
698 response->setPayerEmail(payerEmail);
700 if (m_options.requestPayerPhone)
701 response->setPayerPhone(payerPhone);
703 m_showPromise->resolve(response.get());
704 m_state = State::Closed;
707 void PaymentRequest::complete(std::optional<PaymentComplete>&& result)
709 ASSERT(m_state == State::Closed);
710 std::exchange(m_activePaymentHandler, nullptr)->complete(WTFMove(result));
713 void PaymentRequest::cancel()
715 if (m_state != State::Interactive)
721 m_activePaymentHandler = nullptr;
725 } // namespace WebCore
727 #endif // ENABLE(PAYMENT_REQUEST)