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 auto scope = DECLARE_CATCH_SCOPE(document.execState()->vm());
493 for (auto& paymentMethod : m_serializedMethodData) {
494 auto data = parse(document, paymentMethod.serializedData);
495 ASSERT(!!scope.exception() == data.hasException());
496 if (data.hasException()) {
497 scope.clearException();
501 auto handler = PaymentHandler::create(document, *this, paymentMethod.identifier);
505 auto exception = handler->convertData(data.releaseReturnValue());
506 ASSERT(!!scope.exception() == exception.hasException());
507 if (exception.hasException()) {
508 scope.clearException();
512 handler->canMakePayment([promise = WTFMove(promise)](bool canMakePayment) mutable {
513 promise.resolve(canMakePayment);
518 promise.resolve(false);
521 const String& PaymentRequest::id() const
526 std::optional<PaymentShippingType> PaymentRequest::shippingType() const
528 if (m_options.requestShipping)
529 return m_options.shippingType;
533 bool PaymentRequest::canSuspendForDocumentSuspension() const
537 ASSERT(!m_activePaymentHandler);
539 case State::Interactive:
541 return !m_activePaymentHandler;
545 void PaymentRequest::shippingAddressChanged(Ref<PaymentAddress>&& shippingAddress)
547 whenDetailsSettled([this, protectedThis = makeRefPtr(this), shippingAddress = makeRefPtr(shippingAddress.get())]() mutable {
548 m_shippingAddress = WTFMove(shippingAddress);
549 dispatchEvent(PaymentRequestUpdateEvent::create(eventNames().shippingaddresschangeEvent, *this));
553 void PaymentRequest::shippingOptionChanged(const String& shippingOption)
555 whenDetailsSettled([this, protectedThis = makeRefPtr(this), shippingOption]() mutable {
556 m_shippingOption = shippingOption;
557 dispatchEvent(PaymentRequestUpdateEvent::create(eventNames().shippingoptionchangeEvent, *this));
561 void PaymentRequest::paymentMethodChanged()
563 whenDetailsSettled([this, protectedThis = makeRefPtr(this)] {
564 m_activePaymentHandler->detailsUpdated(UpdateReason::PaymentMethodChanged, { });
568 ExceptionOr<void> PaymentRequest::updateWith(UpdateReason reason, Ref<DOMPromise>&& promise)
570 if (m_state != State::Interactive)
571 return Exception { InvalidStateError };
574 return Exception { InvalidStateError };
578 ASSERT(!m_detailsPromise);
579 m_detailsPromise = WTFMove(promise);
580 m_detailsPromise->whenSettled([this, protectedThis = makeRefPtr(this), reason]() {
581 settleDetailsPromise(reason);
587 ExceptionOr<void> PaymentRequest::completeMerchantValidation(Event& event, Ref<DOMPromise>&& merchantSessionPromise)
589 if (m_state != State::Interactive)
590 return Exception { InvalidStateError };
592 event.stopPropagation();
593 event.stopImmediatePropagation();
595 m_merchantSessionPromise = WTFMove(merchantSessionPromise);
596 m_merchantSessionPromise->whenSettled([this, protectedThis = makeRefPtr(this)]() {
597 if (m_state != State::Interactive)
600 if (m_merchantSessionPromise->status() == DOMPromise::Status::Rejected) {
605 auto exception = m_activePaymentHandler->merchantValidationCompleted(m_merchantSessionPromise->result());
606 if (exception.hasException()) {
607 abortWithException(exception.releaseException());
615 void PaymentRequest::settleDetailsPromise(UpdateReason reason)
617 auto scopeExit = makeScopeExit([&] {
618 m_isUpdating = false;
619 m_detailsPromise = nullptr;
622 if (m_state != State::Interactive)
625 if (m_detailsPromise->status() == DOMPromise::Status::Rejected) {
630 auto& context = *m_detailsPromise->scriptExecutionContext();
631 auto throwScope = DECLARE_THROW_SCOPE(context.vm());
632 auto paymentDetailsUpdate = convertDictionary<PaymentDetailsUpdate>(*context.execState(), m_detailsPromise->result());
633 if (throwScope.exception()) {
634 abortWithException(Exception { ExistingExceptionError });
638 auto totalResult = checkAndCanonicalizeTotal(paymentDetailsUpdate.total.amount);
639 if (totalResult.hasException()) {
640 abortWithException(totalResult.releaseException());
644 auto detailsResult = checkAndCanonicalizeDetails(*context.execState(), paymentDetailsUpdate, m_options.requestShipping, ShouldValidatePaymentMethodIdentifier::Yes);
645 if (detailsResult.hasException()) {
646 abortWithException(detailsResult.releaseException());
650 auto shippingOptionAndModifierData = detailsResult.releaseReturnValue();
652 m_details.total = WTFMove(paymentDetailsUpdate.total);
653 m_details.displayItems = WTFMove(paymentDetailsUpdate.displayItems);
654 if (m_options.requestShipping) {
655 m_details.shippingOptions = WTFMove(paymentDetailsUpdate.shippingOptions);
656 m_shippingOption = WTFMove(std::get<0>(shippingOptionAndModifierData));
659 m_details.modifiers = WTFMove(paymentDetailsUpdate.modifiers);
660 m_serializedModifierData = WTFMove(std::get<1>(shippingOptionAndModifierData));
662 auto result = m_activePaymentHandler->detailsUpdated(reason, paymentDetailsUpdate.error);
663 if (result.hasException()) {
664 abortWithException(result.releaseException());
669 void PaymentRequest::whenDetailsSettled(std::function<void()>&& callback)
671 if (!m_detailsPromise) {
672 ASSERT(m_state == State::Interactive);
673 ASSERT(!m_isUpdating);
678 m_detailsPromise->whenSettled([this, protectedThis = makeRefPtr(this), callback = WTFMove(callback)] {
679 if (m_state != State::Interactive)
682 ASSERT(!m_isUpdating);
687 void PaymentRequest::accept(const String& methodName, JSC::Strong<JSC::JSObject>&& details, Ref<PaymentAddress>&& shippingAddress, const String& payerName, const String& payerEmail, const String& payerPhone)
689 ASSERT(m_state == State::Interactive);
691 auto response = PaymentResponse::create(*this);
692 response->setRequestId(m_details.id);
693 response->setMethodName(methodName);
694 response->setDetails(WTFMove(details));
696 if (m_options.requestShipping) {
697 response->setShippingAddress(shippingAddress.ptr());
698 response->setShippingOption(m_shippingOption);
701 if (m_options.requestPayerName)
702 response->setPayerName(payerName);
704 if (m_options.requestPayerEmail)
705 response->setPayerEmail(payerEmail);
707 if (m_options.requestPayerPhone)
708 response->setPayerPhone(payerPhone);
710 m_showPromise->resolve(response.get());
711 m_state = State::Closed;
714 void PaymentRequest::complete(std::optional<PaymentComplete>&& result)
716 ASSERT(m_state == State::Closed);
717 std::exchange(m_activePaymentHandler, nullptr)->complete(WTFMove(result));
720 void PaymentRequest::cancel()
722 if (m_state != State::Interactive)
728 m_activePaymentHandler = nullptr;
732 } // namespace WebCore
734 #endif // ENABLE(PAYMENT_REQUEST)