| /* |
| * Copyright (C) 2015, 2016 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "ApplePaySession.h" |
| |
| #if ENABLE(APPLE_PAY) |
| |
| #include "ApplePayPaymentAuthorizedEvent.h" |
| #include "ApplePayPaymentMethodSelectedEvent.h" |
| #include "ApplePayShippingContactSelectedEvent.h" |
| #include "ApplePayShippingMethodSelectedEvent.h" |
| #include "ApplePayValidateMerchantEvent.h" |
| #include "ArrayValue.h" |
| #include "DOMWindow.h" |
| #include "Dictionary.h" |
| #include "Document.h" |
| #include "DocumentLoader.h" |
| #include "EventNames.h" |
| #include "JSDOMPromise.h" |
| #include "JSMainThreadExecState.h" |
| #include "LinkIconCollector.h" |
| #include "LinkIconType.h" |
| #include "MainFrame.h" |
| #include "Page.h" |
| #include "PageConsoleClient.h" |
| #include "PaymentAuthorizationStatus.h" |
| #include "PaymentContact.h" |
| #include "PaymentCoordinator.h" |
| #include "PaymentMerchantSession.h" |
| #include "PaymentMethod.h" |
| #include "PaymentRequestValidator.h" |
| #include "ScriptController.h" |
| #include "SecurityOrigin.h" |
| #include "Settings.h" |
| |
| namespace WebCore { |
| |
| static bool parseDigit(UChar digit, bool isNegative, int64_t& amount) |
| { |
| if (!isASCIIDigit(digit)) |
| return false; |
| |
| int64_t digitValue = (digit - '0'); |
| |
| const int64_t maxMultiplier = std::numeric_limits<int64_t>::max() / 10; |
| |
| // Check overflow. |
| if (amount > maxMultiplier || (amount == maxMultiplier && digitValue > (std::numeric_limits<int64_t>::max() % 10) + isNegative)) |
| return false; |
| |
| amount = amount * 10 + digitValue; |
| return true; |
| } |
| |
| // The amount follows the regular expression -?[0-9]+(\.[0-9][0-9])?. |
| static Optional<int64_t> parseAmount(const String& amountString) |
| { |
| int64_t amount = 0; |
| |
| bool isNegative = false; |
| |
| enum class State { |
| Start, |
| Sign, |
| Digit, |
| Dot, |
| DotDigit, |
| End, |
| }; |
| |
| State state = State::Start; |
| |
| for (unsigned i = 0; i < amountString.length(); ++i) { |
| UChar c = amountString[i]; |
| |
| switch (state) { |
| case State::Start: |
| if (c == '-') { |
| isNegative = true; |
| state = State::Sign; |
| break; |
| } |
| |
| if (!parseDigit(c, isNegative, amount)) |
| return Nullopt; |
| state = State::Digit; |
| break; |
| |
| case State::Sign: |
| if (!parseDigit(c, isNegative, amount)) |
| return Nullopt; |
| state = State::Digit; |
| break; |
| |
| case State::Digit: |
| if (c == '.') { |
| state = State::Dot; |
| break; |
| } |
| |
| if (!parseDigit(c, isNegative, amount)) |
| return Nullopt; |
| break; |
| |
| case State::Dot: |
| if (!parseDigit(c, isNegative, amount)) |
| return Nullopt; |
| |
| state = State::DotDigit; |
| break; |
| |
| case State::DotDigit: |
| if (!parseDigit(c, isNegative, amount)) |
| return Nullopt; |
| |
| state = State::End; |
| break; |
| |
| case State::End: |
| return Nullopt; |
| } |
| } |
| |
| if (state != State::Digit && state != State::DotDigit && state != State::End) |
| return Nullopt; |
| |
| if (state == State::DotDigit) { |
| // There was a single digit after the decimal point. |
| // FIXME: Handle this overflowing. |
| amount *= 10; |
| } else if (state == State::Digit) { |
| // There was no decimal point. |
| // FIXME: Handle this overflowing. |
| amount *= 100; |
| } |
| |
| if (isNegative) |
| amount = -amount; |
| |
| return amount; |
| } |
| |
| static Optional<PaymentRequest::ContactFields> createContactFields(DOMWindow& window, const ArrayValue& contactFieldsArray) |
| { |
| PaymentRequest::ContactFields result; |
| |
| size_t contactFieldsCount; |
| if (!contactFieldsArray.length(contactFieldsCount)) |
| return Nullopt; |
| |
| for (size_t i = 0; i < contactFieldsCount; ++i) { |
| String contactField; |
| if (!contactFieldsArray.get(i, contactField)) |
| return Nullopt; |
| |
| if (contactField == "postalAddress") |
| result.postalAddress = true; |
| else if (contactField == "phone") |
| result.phone = true; |
| else if (contactField == "email") |
| result.email = true; |
| else if (contactField == "name") |
| result.name = true; |
| else { |
| auto message = makeString("\"" + contactField, "\" is not a valid contact field."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| return result; |
| } |
| |
| static Optional<PaymentRequest::LineItem::Type> toLineItemType(const String& type) |
| { |
| if (type == "pending") |
| return PaymentRequest::LineItem::Type::Pending; |
| if (type == "final") |
| return PaymentRequest::LineItem::Type::Final; |
| |
| return Nullopt; |
| } |
| |
| static bool isValidLineItemPropertyName(const String& propertyName) |
| { |
| const char* validPropertyNames[] = { |
| "type", |
| "label", |
| "amount", |
| }; |
| |
| for (auto& validPropertyName : validPropertyNames) { |
| if (propertyName == validPropertyName) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static Optional<PaymentRequest::LineItem> createLineItem(DOMWindow& window, const Dictionary& total) |
| { |
| Vector<String> propertyNames; |
| total.getOwnPropertyNames(propertyNames); |
| |
| for (auto& propertyName : propertyNames) { |
| if (!isValidLineItemPropertyName(propertyName)) { |
| auto message = makeString("\"" + propertyName, "\" is not a valid line item property name."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| // Line item type defaults to Final. |
| PaymentRequest::LineItem result; |
| |
| if (auto typeString = total.get<String>("type")) { |
| auto type = toLineItemType(*typeString); |
| if (!type) { |
| auto message = makeString("\"" + *typeString, "\" is not a valid line item type."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| |
| result.type = *type; |
| } |
| |
| if (auto label = total.get<String>("label")) |
| result.label = *label; |
| if (auto amountString = total.get<String>("amount")) { |
| if (auto amount = parseAmount(*amountString)) |
| result.amount = *amount; |
| else { |
| auto message = makeString("\"" + *amountString, "\" is not a valid amount."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| return result; |
| } |
| |
| static Optional<Vector<PaymentRequest::LineItem>> createLineItems(DOMWindow& window, const ArrayValue& lineItemsArray) |
| { |
| Vector<PaymentRequest::LineItem> result; |
| |
| size_t lineItemCount; |
| if (!lineItemsArray.length(lineItemCount)) |
| return Nullopt; |
| |
| for (size_t i = 0; i < lineItemCount; ++i) { |
| Dictionary lineItemDictionary; |
| if (!lineItemsArray.get(i, lineItemDictionary)) |
| return Nullopt; |
| |
| if (auto lineItem = createLineItem(window, lineItemDictionary)) |
| result.append(*lineItem); |
| } |
| |
| return result; |
| } |
| |
| static Optional<PaymentRequest::MerchantCapabilities> createMerchantCapabilities(DOMWindow& window, const ArrayValue& merchantCapabilitiesArray) |
| { |
| PaymentRequest::MerchantCapabilities result; |
| |
| size_t merchantCapabilitiesCount; |
| if (!merchantCapabilitiesArray.length(merchantCapabilitiesCount)) |
| return Nullopt; |
| |
| for (size_t i = 0; i < merchantCapabilitiesCount; ++i) { |
| String merchantCapability; |
| if (!merchantCapabilitiesArray.get(i, merchantCapability)) |
| return Nullopt; |
| |
| if (merchantCapability == "supports3DS") |
| result.supports3DS = true; |
| else if (merchantCapability == "supportsEMV") |
| result.supportsEMV = true; |
| else if (merchantCapability == "supportsCredit") |
| result.supportsCredit = true; |
| else if (merchantCapability == "supportsDebit") |
| result.supportsDebit = true; |
| else { |
| auto message = makeString("\"" + merchantCapability, "\" is not a valid merchant capability."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| return result; |
| } |
| |
| static Optional<Vector<String>> createSupportedNetworks(unsigned version, DOMWindow& window, const ArrayValue& supportedNetworksArray) |
| { |
| Vector<String> result; |
| |
| size_t supportedNetworksCount; |
| if (!supportedNetworksArray.length(supportedNetworksCount)) |
| return Nullopt; |
| |
| for (size_t i = 0; i < supportedNetworksCount; ++i) { |
| String supportedNetwork; |
| if (!supportedNetworksArray.get(i, supportedNetwork)) |
| return Nullopt; |
| |
| if (!PaymentRequest::isValidSupportedNetwork(version, supportedNetwork)) { |
| auto message = makeString("\"" + supportedNetwork, "\" is not a valid payment network."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| |
| result.append(WTFMove(supportedNetwork)); |
| } |
| |
| return result; |
| } |
| |
| static Optional<PaymentRequest::ShippingType> toShippingType(const String& shippingTypeString) |
| { |
| if (shippingTypeString == "shipping") |
| return PaymentRequest::ShippingType::Shipping; |
| if (shippingTypeString == "delivery") |
| return PaymentRequest::ShippingType::Delivery; |
| if (shippingTypeString == "storePickup") |
| return PaymentRequest::ShippingType::StorePickup; |
| if (shippingTypeString == "servicePickup") |
| return PaymentRequest::ShippingType::ServicePickup; |
| |
| return Nullopt; |
| } |
| |
| static bool isValidShippingMethodPropertyName(const String& propertyName) |
| { |
| const char* validPropertyNames[] = { |
| "label", |
| "detail", |
| "amount", |
| "identifier", |
| }; |
| |
| for (auto& validPropertyName : validPropertyNames) { |
| if (propertyName == validPropertyName) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static Optional<PaymentRequest::ShippingMethod> createShippingMethod(DOMWindow& window, const Dictionary& shippingMethodDictionary) |
| { |
| Vector<String> propertyNames; |
| shippingMethodDictionary.getOwnPropertyNames(propertyNames); |
| |
| for (auto& propertyName : propertyNames) { |
| if (!isValidShippingMethodPropertyName(propertyName)) { |
| auto message = makeString("\"" + propertyName, "\" is not a valid shipping method property name."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| PaymentRequest::ShippingMethod result; |
| |
| auto label = shippingMethodDictionary.get<String>("label"); |
| if (!label) { |
| window.printErrorMessage("Missing shipping method label."); |
| return Nullopt; |
| } |
| result.label = *label; |
| |
| auto detail = shippingMethodDictionary.get<String>("detail"); |
| if (!detail) { |
| window.printErrorMessage("Missing shipping method detail."); |
| return Nullopt; |
| } |
| result.detail = *detail; |
| |
| auto amountString = shippingMethodDictionary.get<String>("amount"); |
| if (!amountString) { |
| window.printErrorMessage("Missing shipping method amount."); |
| return Nullopt; |
| } |
| |
| if (auto amount = parseAmount(*amountString)) |
| result.amount = *amount; |
| else { |
| auto message = makeString("\"" + *amountString, "\" is not a valid amount."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| |
| auto identifier = shippingMethodDictionary.get<String>("identifier"); |
| if (!identifier) { |
| window.printErrorMessage("Missing shipping method identifier."); |
| return Nullopt; |
| } |
| result.identifier = *identifier; |
| |
| return result; |
| } |
| |
| static Optional<Vector<PaymentRequest::ShippingMethod>> createShippingMethods(DOMWindow& window, const ArrayValue& shippingMethodsArray) |
| { |
| Vector<PaymentRequest::ShippingMethod> result; |
| |
| size_t shippingMethodCount; |
| if (!shippingMethodsArray.length(shippingMethodCount)) |
| return Nullopt; |
| |
| for (size_t i = 0; i < shippingMethodCount; ++i) { |
| Dictionary shippingMethodDictionary; |
| if (!shippingMethodsArray.get(i, shippingMethodDictionary)) |
| return Nullopt; |
| |
| if (auto shippingMethod = createShippingMethod(window, shippingMethodDictionary)) |
| result.append(*shippingMethod); |
| else |
| return Nullopt; |
| } |
| |
| return result; |
| } |
| |
| static bool isValidPaymentRequestPropertyName(const String& propertyName) |
| { |
| const char* validPropertyNames[] = { |
| "merchantCapabilities", |
| "supportedNetworks", |
| "countryCode", |
| "currencyCode", |
| "requiredBillingContactFields", |
| "billingContact", |
| "requiredShippingContactFields", |
| "shippingContact", |
| "shippingType", |
| "shippingMethods", |
| "total", |
| "lineItems", |
| "applicationData", |
| }; |
| |
| for (auto& validPropertyName : validPropertyNames) { |
| if (propertyName == validPropertyName) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static Optional<PaymentRequest> createPaymentRequest(unsigned version, DOMWindow& window, const Dictionary& dictionary) |
| { |
| PaymentRequest paymentRequest; |
| |
| Vector<String> propertyNames; |
| dictionary.getOwnPropertyNames(propertyNames); |
| |
| for (auto& propertyName : propertyNames) { |
| if (propertyName == "requiredShippingAddressFields") { |
| window.printErrorMessage("\"requiredShippingAddressFields\" has been deprecated. Please switch to \"requiredShippingContactFields\" instead."); |
| return Nullopt; |
| } |
| |
| if (propertyName == "requiredBillingAddressFields") { |
| window.printErrorMessage("\"requiredBillingAddressFields\" has been deprecated. Please switch to \"requiredBillingContactFields\" instead."); |
| return Nullopt; |
| } |
| |
| if (!isValidPaymentRequestPropertyName(propertyName)) { |
| auto message = makeString("\"" + propertyName, "\" is not a valid payment request property name."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| } |
| |
| if (auto merchantCapabilitiesArray = dictionary.get<ArrayValue>("merchantCapabilities")) { |
| auto merchantCapabilities = createMerchantCapabilities(window, *merchantCapabilitiesArray); |
| if (!merchantCapabilities) |
| return Nullopt; |
| |
| paymentRequest.setMerchantCapabilities(*merchantCapabilities); |
| } |
| |
| if (auto supportedNetworksArray = dictionary.get<ArrayValue>("supportedNetworks")) { |
| auto supportedNetworks = createSupportedNetworks(version, window, *supportedNetworksArray); |
| if (!supportedNetworks) |
| return Nullopt; |
| |
| paymentRequest.setSupportedNetworks(*supportedNetworks); |
| } |
| |
| if (auto countryCode = dictionary.get<String>("countryCode")) |
| paymentRequest.setCountryCode(*countryCode); |
| if (auto currencyCode = dictionary.get<String>("currencyCode")) |
| paymentRequest.setCurrencyCode(*currencyCode); |
| |
| if (auto requiredBillingContactFieldsArray = dictionary.get<ArrayValue>("requiredBillingContactFields")) { |
| auto requiredBillingContactFields = createContactFields(window, *requiredBillingContactFieldsArray); |
| if (!requiredBillingContactFields) |
| return Nullopt; |
| |
| paymentRequest.setRequiredBillingContactFields(*requiredBillingContactFields); |
| } |
| |
| if (auto billingContactValue = dictionary.get<JSC::JSValue>("billingContact")) { |
| String errorMessage; |
| auto billingContact = PaymentContact::fromJS(*JSMainThreadExecState::currentState(), *billingContactValue, errorMessage); |
| if (!billingContact) { |
| window.printErrorMessage(errorMessage); |
| return Nullopt; |
| } |
| |
| paymentRequest.setBillingContact(*billingContact); |
| } |
| |
| if (auto requiredShippingContactFieldsArray = dictionary.get<ArrayValue>("requiredShippingContactFields")) { |
| auto requiredShippingContactFields = createContactFields(window, *requiredShippingContactFieldsArray); |
| if (!requiredShippingContactFields) |
| return Nullopt; |
| |
| paymentRequest.setRequiredShippingContactFields(*requiredShippingContactFields); |
| } |
| |
| if (auto shippingContactValue = dictionary.get<JSC::JSValue>("shippingContact")) { |
| String errorMessage; |
| auto shippingContact = PaymentContact::fromJS(*JSMainThreadExecState::currentState(), *shippingContactValue, errorMessage); |
| if (!shippingContact) { |
| window.printErrorMessage(errorMessage); |
| return Nullopt; |
| } |
| |
| paymentRequest.setShippingContact(*shippingContact); |
| } |
| |
| if (auto shippingTypeString = dictionary.get<String>("shippingType")) { |
| auto shippingType = toShippingType(*shippingTypeString); |
| |
| if (!shippingType) { |
| auto message = makeString("\"" + *shippingTypeString, "\" is not a valid shipping type."); |
| window.printErrorMessage(message); |
| return Nullopt; |
| } |
| paymentRequest.setShippingType(*shippingType); |
| } |
| |
| if (auto shippingMethodsArray = dictionary.get<ArrayValue>("shippingMethods")) { |
| auto shippingMethods = createShippingMethods(window, *shippingMethodsArray); |
| if (!shippingMethods) |
| return Nullopt; |
| |
| paymentRequest.setShippingMethods(*shippingMethods); |
| } |
| |
| if (auto totalDictionary = dictionary.get<Dictionary>("total")) { |
| auto total = createLineItem(window, *totalDictionary); |
| if (!total) |
| return Nullopt; |
| |
| paymentRequest.setTotal(*total); |
| } |
| |
| if (auto lineItemsArray = dictionary.get<ArrayValue>("lineItems")) { |
| if (auto lineItems = createLineItems(window, *lineItemsArray)) |
| paymentRequest.setLineItems(*lineItems); |
| } |
| |
| if (auto applicationData = dictionary.get<String>("applicationData")) |
| paymentRequest.setApplicationData(*applicationData); |
| |
| return paymentRequest; |
| } |
| |
| static bool isSecure(DocumentLoader& documentLoader) |
| { |
| if (!documentLoader.response().url().protocolIs("https")) |
| return false; |
| |
| if (!documentLoader.response().certificateInfo() || documentLoader.response().certificateInfo()->containsNonRootSHA1SignedCertificate()) |
| return false; |
| |
| return true; |
| } |
| |
| static bool canCallApplePaySessionAPIs(Document& document, String& errorMessage) |
| { |
| if (!isSecure(*document.loader())) { |
| errorMessage = "Trying to call an ApplePaySession API from an insecure document."; |
| return false; |
| } |
| |
| auto& topDocument = document.topDocument(); |
| if (&document != &topDocument) { |
| auto& topOrigin = *topDocument.topOrigin(); |
| |
| if (!document.securityOrigin()->isSameSchemeHostPort(&topOrigin)) { |
| errorMessage = "Trying to call an ApplePaySession API from a document with an different security origin than its top-level frame."; |
| return false; |
| } |
| |
| for (auto* ancestorDocument = document.parentDocument(); ancestorDocument != &topDocument; ancestorDocument = ancestorDocument->parentDocument()) { |
| if (!isSecure(*ancestorDocument->loader())) { |
| errorMessage = "Trying to call an ApplePaySession API from a document with an insecure parent frame."; |
| return false; |
| } |
| |
| if (!ancestorDocument->securityOrigin()->isSameSchemeHostPort(&topOrigin)) { |
| errorMessage = "Trying to call an ApplePaySession API from a document with an different security origin than its top-level frame."; |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| ExceptionOr<Ref<ApplePaySession>> ApplePaySession::create(Document& document, unsigned version, const Dictionary& dictionary) |
| { |
| DOMWindow& window = *document.domWindow(); |
| |
| String errorMessage; |
| if (!canCallApplePaySessionAPIs(document, errorMessage)) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| if (!ScriptController::processingUserGesture()) { |
| window.printErrorMessage("Must create a new ApplePaySession from a user gesture handler."); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator(); |
| |
| if (!version || !paymentCoordinator.supportsVersion(version)) { |
| window.printErrorMessage(makeString("\"" + String::number(version), "\" is not a supported version.")); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| auto paymentRequest = createPaymentRequest(version, window, dictionary); |
| if (!paymentRequest) |
| return Exception { TYPE_MISMATCH_ERR }; |
| |
| if (!PaymentRequestValidator(window).validate(*paymentRequest)) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| return adoptRef(*new ApplePaySession(document, WTFMove(*paymentRequest))); |
| } |
| |
| ApplePaySession::ApplePaySession(Document& document, PaymentRequest&& paymentRequest) |
| : ActiveDOMObject(&document) |
| , m_paymentRequest(WTFMove(paymentRequest)) |
| { |
| suspendIfNeeded(); |
| } |
| |
| ApplePaySession::~ApplePaySession() |
| { |
| } |
| |
| ExceptionOr<bool> ApplePaySession::supportsVersion(ScriptExecutionContext& scriptExecutionContext, unsigned version) |
| { |
| if (!version) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto& document = downcast<Document>(scriptExecutionContext); |
| DOMWindow& window = *document.domWindow(); |
| |
| String errorMessage; |
| if (!canCallApplePaySessionAPIs(document, errorMessage)) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| return document.frame()->mainFrame().paymentCoordinator().supportsVersion(version); |
| } |
| |
| static bool shouldDiscloseApplePayCapability(Document& document) |
| { |
| auto* page = document.page(); |
| if (!page || page->usesEphemeralSession()) |
| return false; |
| |
| return document.frame()->settings().applePayCapabilityDisclosureAllowed(); |
| } |
| |
| ExceptionOr<bool> ApplePaySession::canMakePayments(ScriptExecutionContext& scriptExecutionContext) |
| { |
| auto& document = downcast<Document>(scriptExecutionContext); |
| DOMWindow& window = *document.domWindow(); |
| |
| String errorMessage; |
| if (!canCallApplePaySessionAPIs(document, errorMessage)) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| return document.frame()->mainFrame().paymentCoordinator().canMakePayments(); |
| } |
| |
| ExceptionOr<void> ApplePaySession::canMakePaymentsWithActiveCard(ScriptExecutionContext& scriptExecutionContext, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise) |
| { |
| auto& document = downcast<Document>(scriptExecutionContext); |
| DOMWindow& window = *document.domWindow(); |
| |
| String errorMessage; |
| if (!canCallApplePaySessionAPIs(document, errorMessage)) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| RefPtr<DeferredPromise> promise(WTFMove(passedPromise)); |
| if (!shouldDiscloseApplePayCapability(document)) { |
| auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator(); |
| bool canMakePayments = paymentCoordinator.canMakePayments(); |
| |
| RunLoop::main().dispatch([promise, canMakePayments]() mutable { |
| promise->resolve(canMakePayments); |
| }); |
| return { }; |
| } |
| |
| auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator(); |
| |
| paymentCoordinator.canMakePaymentsWithActiveCard(merchantIdentifier, document.domain(), [promise](bool canMakePayments) mutable { |
| promise->resolve(canMakePayments); |
| }); |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::openPaymentSetup(ScriptExecutionContext& scriptExecutionContext, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise) |
| { |
| auto& document = downcast<Document>(scriptExecutionContext); |
| DOMWindow& window = *document.domWindow(); |
| |
| String errorMessage; |
| if (!canCallApplePaySessionAPIs(document, errorMessage)) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| if (!ScriptController::processingUserGesture()) { |
| window.printErrorMessage("Must call ApplePaySession.openPaymentSetup from a user gesture handler."); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| RefPtr<DeferredPromise> promise(WTFMove(passedPromise)); |
| auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator(); |
| |
| paymentCoordinator.openPaymentSetup(merchantIdentifier, document.domain(), [promise](bool result) mutable { |
| promise->resolve(result); |
| }); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::begin() |
| { |
| auto& document = *downcast<Document>(scriptExecutionContext()); |
| auto& window = *document.domWindow(); |
| |
| if (!canBegin()) { |
| window.printErrorMessage("Payment session is already active."); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| if (paymentCoordinator().hasActiveSession()) { |
| window.printErrorMessage("Page already has an active payment session."); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| Vector<URL> linkIconURLs; |
| for (auto& icon : LinkIconCollector { document }.iconsOfTypes({ LinkIconType::TouchIcon, LinkIconType::TouchPrecomposedIcon })) |
| linkIconURLs.append(icon.url); |
| |
| if (!paymentCoordinator().beginPaymentSession(*this, document.url(), linkIconURLs, m_paymentRequest)) { |
| window.printErrorMessage("There is already has an active payment session."); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| m_state = State::Active; |
| |
| setPendingActivity(this); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::abort() |
| { |
| if (!canAbort()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| m_state = State::Aborted; |
| paymentCoordinator().abortPaymentSession(); |
| |
| didReachFinalState(); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::completeMerchantValidation(const Dictionary& merchantSessionDictionary) |
| { |
| if (!canCompleteMerchantValidation()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| if (!merchantSessionDictionary.initializerObject()) |
| return Exception { TypeError }; |
| |
| auto& document = *downcast<Document>(scriptExecutionContext()); |
| auto& window = *document.domWindow(); |
| |
| String errorMessage; |
| auto merchantSession = PaymentMerchantSession::fromJS(*merchantSessionDictionary.execState(), merchantSessionDictionary.initializerObject(), errorMessage); |
| if (!merchantSession) { |
| window.printErrorMessage(errorMessage); |
| return Exception { INVALID_ACCESS_ERR }; |
| } |
| |
| m_merchantValidationState = MerchantValidationState::ValidationComplete; |
| paymentCoordinator().completeMerchantValidation(*merchantSession); |
| |
| return { }; |
| } |
| |
| static Optional<PaymentAuthorizationStatus> toPaymentAuthorizationStatus(unsigned short status) |
| { |
| switch (status) { |
| case ApplePaySession::STATUS_SUCCESS: |
| return PaymentAuthorizationStatus::Success; |
| |
| case ApplePaySession::STATUS_FAILURE: |
| return PaymentAuthorizationStatus::Failure; |
| |
| case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS: |
| return PaymentAuthorizationStatus::InvalidBillingPostalAddress; |
| |
| case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS: |
| return PaymentAuthorizationStatus::InvalidShippingPostalAddress; |
| |
| case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT: |
| return PaymentAuthorizationStatus::InvalidShippingContact; |
| |
| case ApplePaySession::STATUS_PIN_REQUIRED: |
| return PaymentAuthorizationStatus::PINRequired; |
| |
| case ApplePaySession::STATUS_PIN_INCORRECT: |
| return PaymentAuthorizationStatus::PINIncorrect; |
| |
| case ApplePaySession::STATUS_PIN_LOCKOUT: |
| return PaymentAuthorizationStatus::PINLockout; |
| |
| default: |
| return Nullopt; |
| } |
| } |
| |
| ExceptionOr<void> ApplePaySession::completeShippingMethodSelection(unsigned short status, const Dictionary& newTotalDictionary, const ArrayValue& newLineItemsArray) |
| { |
| if (!canCompleteShippingMethodSelection()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto authorizationStatus = toPaymentAuthorizationStatus(status); |
| if (!authorizationStatus) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto& window = *downcast<Document>(scriptExecutionContext())->domWindow(); |
| auto newTotal = createLineItem(window, newTotalDictionary); |
| if (!newTotal) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| if (!PaymentRequestValidator(window).validateTotal(*newTotal)) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto newLineItems = createLineItems(window, newLineItemsArray); |
| if (!newLineItems) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| m_state = State::Active; |
| PaymentRequest::TotalAndLineItems totalAndLineItems; |
| totalAndLineItems.total = *newTotal; |
| totalAndLineItems.lineItems = *newLineItems; |
| paymentCoordinator().completeShippingMethodSelection(*authorizationStatus, totalAndLineItems); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::completeShippingContactSelection(unsigned short status, const ArrayValue& newShippingMethodsArray, const Dictionary& newTotalDictionary, const ArrayValue& newLineItemsArray) |
| { |
| if (!canCompleteShippingContactSelection()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto authorizationStatus = toPaymentAuthorizationStatus(status); |
| if (!authorizationStatus) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto& window = *downcast<Document>(scriptExecutionContext())->domWindow(); |
| |
| auto newShippingMethods = createShippingMethods(window, newShippingMethodsArray); |
| if (!newShippingMethods) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto newTotal = createLineItem(window, newTotalDictionary); |
| if (!newTotal) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| if (!PaymentRequestValidator(window).validateTotal(*newTotal)) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto newLineItems = createLineItems(window, newLineItemsArray); |
| if (!newLineItems) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| m_state = State::Active; |
| PaymentRequest::TotalAndLineItems totalAndLineItems; |
| totalAndLineItems.total = *newTotal; |
| totalAndLineItems.lineItems = *newLineItems; |
| paymentCoordinator().completeShippingContactSelection(*authorizationStatus, *newShippingMethods, totalAndLineItems); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::completePaymentMethodSelection(const Dictionary& newTotalDictionary, const ArrayValue& newLineItemsArray) |
| { |
| if (!canCompletePaymentMethodSelection()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto& window = *downcast<Document>(*scriptExecutionContext()).domWindow(); |
| auto newTotal = createLineItem(window, newTotalDictionary); |
| if (!newTotal) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| if (!PaymentRequestValidator(window).validateTotal(*newTotal)) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto newLineItems = createLineItems(window, newLineItemsArray); |
| if (!newLineItems) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| m_state = State::Active; |
| PaymentRequest::TotalAndLineItems totalAndLineItems; |
| totalAndLineItems.total = *newTotal; |
| totalAndLineItems.lineItems = *newLineItems; |
| paymentCoordinator().completePaymentMethodSelection(totalAndLineItems); |
| |
| return { }; |
| } |
| |
| ExceptionOr<void> ApplePaySession::completePayment(unsigned short status) |
| { |
| if (!canCompletePayment()) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| auto authorizationStatus = toPaymentAuthorizationStatus(status); |
| if (!authorizationStatus) |
| return Exception { INVALID_ACCESS_ERR }; |
| |
| paymentCoordinator().completePaymentSession(*authorizationStatus); |
| |
| if (!isFinalStateStatus(*authorizationStatus)) { |
| m_state = State::Active; |
| return { }; |
| } |
| |
| m_state = State::Completed; |
| unsetPendingActivity(this); |
| return { }; |
| } |
| |
| void ApplePaySession::validateMerchant(const URL& validationURL) |
| { |
| if (m_state == State::Aborted) { |
| // ApplePaySession::abort has been called. |
| return; |
| } |
| |
| ASSERT(m_merchantValidationState == MerchantValidationState::Idle); |
| ASSERT(m_state == State::Active); |
| |
| if (validationURL.isNull()) { |
| // Something went wrong when getting the validation URL. |
| // FIXME: Maybe we should send an error event here instead? |
| return; |
| } |
| |
| m_merchantValidationState = MerchantValidationState::ValidatingMerchant; |
| |
| auto event = ApplePayValidateMerchantEvent::create(eventNames().validatemerchantEvent, validationURL); |
| dispatchEvent(event.get()); |
| } |
| |
| void ApplePaySession::didAuthorizePayment(const Payment& payment) |
| { |
| ASSERT(m_state == State::Active); |
| |
| m_state = State::Authorized; |
| |
| auto event = ApplePayPaymentAuthorizedEvent::create(eventNames().paymentauthorizedEvent, payment); |
| dispatchEvent(event.get()); |
| } |
| |
| void ApplePaySession::didSelectShippingMethod(const PaymentRequest::ShippingMethod& shippingMethod) |
| { |
| ASSERT(m_state == State::Active); |
| |
| if (!hasEventListeners(eventNames().shippingmethodselectedEvent)) { |
| paymentCoordinator().completeShippingMethodSelection(PaymentAuthorizationStatus::Success, { }); |
| return; |
| } |
| |
| m_state = State::ShippingMethodSelected; |
| auto event = ApplePayShippingMethodSelectedEvent::create(eventNames().shippingmethodselectedEvent, shippingMethod); |
| dispatchEvent(event.get()); |
| } |
| |
| void ApplePaySession::didSelectShippingContact(const PaymentContact& shippingContact) |
| { |
| ASSERT(m_state == State::Active); |
| |
| if (!hasEventListeners(eventNames().shippingcontactselectedEvent)) { |
| paymentCoordinator().completeShippingContactSelection(PaymentAuthorizationStatus::Success, { }, { }); |
| return; |
| } |
| |
| m_state = State::ShippingContactSelected; |
| auto event = ApplePayShippingContactSelectedEvent::create(eventNames().shippingcontactselectedEvent, shippingContact); |
| dispatchEvent(event.get()); |
| } |
| |
| void ApplePaySession::didSelectPaymentMethod(const PaymentMethod& paymentMethod) |
| { |
| ASSERT(m_state == State::Active); |
| |
| if (!hasEventListeners(eventNames().paymentmethodselectedEvent)) { |
| paymentCoordinator().completePaymentMethodSelection({ }); |
| return; |
| } |
| |
| m_state = State::PaymentMethodSelected; |
| auto event = ApplePayPaymentMethodSelectedEvent::create(eventNames().paymentmethodselectedEvent, paymentMethod); |
| dispatchEvent(event.get()); |
| } |
| |
| void ApplePaySession::didCancelPayment() |
| { |
| ASSERT(canCancel()); |
| |
| m_state = State::Canceled; |
| |
| auto event = Event::create(eventNames().cancelEvent, false, false); |
| dispatchEvent(event.get()); |
| |
| didReachFinalState(); |
| } |
| |
| const char* ApplePaySession::activeDOMObjectName() const |
| { |
| return "ApplePaySession"; |
| } |
| |
| bool ApplePaySession::canSuspendForDocumentSuspension() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Completed: |
| case State::Canceled: |
| return true; |
| |
| case State::Active: |
| case State::Authorized: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| return false; |
| } |
| } |
| |
| void ApplePaySession::stop() |
| { |
| if (!canAbort()) |
| return; |
| |
| m_state = State::Aborted; |
| paymentCoordinator().abortPaymentSession(); |
| |
| didReachFinalState(); |
| } |
| |
| PaymentCoordinator& ApplePaySession::paymentCoordinator() const |
| { |
| return downcast<Document>(*scriptExecutionContext()).frame()->mainFrame().paymentCoordinator(); |
| } |
| |
| bool ApplePaySession::canBegin() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| return true; |
| |
| case State::Active: |
| case State::Aborted: |
| case State::Authorized: |
| case State::Completed: |
| case State::Canceled: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| return false; |
| } |
| } |
| |
| bool ApplePaySession::canAbort() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Completed: |
| case State::Canceled: |
| return false; |
| |
| case State::Active: |
| case State::Authorized: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::canCancel() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Completed: |
| case State::Canceled: |
| return false; |
| |
| case State::Active: |
| case State::Authorized: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::canCompleteMerchantValidation() const |
| { |
| if (m_state != State::Active) |
| return false; |
| |
| if (m_merchantValidationState != MerchantValidationState::ValidatingMerchant) |
| return false; |
| |
| return true; |
| } |
| |
| bool ApplePaySession::canCompleteShippingMethodSelection() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Active: |
| case State::Completed: |
| case State::Canceled: |
| case State::Authorized: |
| case State::PaymentMethodSelected: |
| case State::ShippingContactSelected: |
| return false; |
| |
| case State::ShippingMethodSelected: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::canCompleteShippingContactSelection() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Active: |
| case State::Completed: |
| case State::Canceled: |
| case State::Authorized: |
| case State::PaymentMethodSelected: |
| case State::ShippingMethodSelected: |
| return false; |
| |
| case State::ShippingContactSelected: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::canCompletePaymentMethodSelection() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Active: |
| case State::Completed: |
| case State::Canceled: |
| case State::Authorized: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| return false; |
| |
| case State::PaymentMethodSelected: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::canCompletePayment() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Aborted: |
| case State::Active: |
| case State::Completed: |
| case State::Canceled: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| return false; |
| |
| case State::Authorized: |
| return true; |
| } |
| } |
| |
| bool ApplePaySession::isFinalState() const |
| { |
| switch (m_state) { |
| case State::Idle: |
| case State::Active: |
| case State::ShippingMethodSelected: |
| case State::ShippingContactSelected: |
| case State::PaymentMethodSelected: |
| case State::Authorized: |
| return false; |
| |
| case State::Completed: |
| case State::Aborted: |
| case State::Canceled: |
| return true; |
| } |
| } |
| |
| void ApplePaySession::didReachFinalState() |
| { |
| ASSERT(isFinalState()); |
| unsetPendingActivity(this); |
| } |
| |
| } |
| |
| #endif |