blob: 08f404e18dc0d490d8ad3d28574aeb923ab783ef [file] [log] [blame]
/*
* Copyright (C) 2017-2018 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 "ApplePayPaymentHandler.h"
#if ENABLE(APPLE_PAY) && ENABLE(PAYMENT_REQUEST)
#include "ApplePayContactField.h"
#include "ApplePayMerchantCapability.h"
#include "ApplePayModifier.h"
#include "ApplePayPayment.h"
#include "ApplePaySessionPaymentRequest.h"
#include "Document.h"
#include "EventNames.h"
#include "Frame.h"
#include "JSApplePayPayment.h"
#include "JSApplePayRequest.h"
#include "LinkIconCollector.h"
#include "MerchantValidationEvent.h"
#include "Page.h"
#include "Payment.h"
#include "PaymentAuthorizationStatus.h"
#include "PaymentContact.h"
#include "PaymentCoordinator.h"
#include "PaymentMerchantSession.h"
#include "PaymentMethod.h"
#include "PaymentRequestValidator.h"
#include "PaymentResponse.h"
#include "Settings.h"
namespace WebCore {
bool ApplePayPaymentHandler::handlesIdentifier(const PaymentRequest::MethodIdentifier& identifier)
{
if (!WTF::holds_alternative<URL>(identifier))
return false;
auto& url = WTF::get<URL>(identifier);
return url.host() == "apple.com" && url.path() == "/apple-pay";
}
static inline PaymentCoordinator& paymentCoordinator(Document& document)
{
ASSERT(document.page());
return document.page()->paymentCoordinator();
}
bool ApplePayPaymentHandler::hasActiveSession(Document& document)
{
return WebCore::paymentCoordinator(document).hasActiveSession();
}
ApplePayPaymentHandler::ApplePayPaymentHandler(Document& document, const PaymentRequest::MethodIdentifier& identifier, PaymentRequest& paymentRequest)
: ContextDestructionObserver { &document }
, m_identifier { identifier }
, m_paymentRequest { paymentRequest }
{
ASSERT(handlesIdentifier(m_identifier));
}
Document& ApplePayPaymentHandler::document() const
{
return downcast<Document>(*scriptExecutionContext());
}
PaymentCoordinator& ApplePayPaymentHandler::paymentCoordinator() const
{
return WebCore::paymentCoordinator(document());
}
static ExceptionOr<void> validate(const PaymentCurrencyAmount& amount, const String& expectedCurrency)
{
if (amount.currency != expectedCurrency)
return Exception { TypeError, makeString("\"", amount.currency, "\" does not match the expected currency of \"", expectedCurrency, "\". Apple Pay requires all PaymentCurrencyAmounts to use the same currency code.") };
return { };
}
static ExceptionOr<ApplePaySessionPaymentRequest::LineItem> convertAndValidate(const PaymentItem& item, const String& expectedCurrency)
{
auto exception = validate(item.amount, expectedCurrency);
if (exception.hasException())
return exception.releaseException();
ApplePaySessionPaymentRequest::LineItem lineItem;
lineItem.amount = item.amount.value;
lineItem.type = item.pending ? ApplePaySessionPaymentRequest::LineItem::Type::Pending : ApplePaySessionPaymentRequest::LineItem::Type::Final;
lineItem.label = item.label;
return { WTFMove(lineItem) };
}
static ExceptionOr<Vector<ApplePaySessionPaymentRequest::LineItem>> convertAndValidate(const Vector<PaymentItem>& lineItems, const String& expectedCurrency)
{
Vector<ApplePaySessionPaymentRequest::LineItem> result;
result.reserveInitialCapacity(lineItems.size());
for (auto& lineItem : lineItems) {
auto convertedLineItem = convertAndValidate(lineItem, expectedCurrency);
if (convertedLineItem.hasException())
return convertedLineItem.releaseException();
result.uncheckedAppend(convertedLineItem.releaseReturnValue());
}
return { WTFMove(result) };
}
static ApplePaySessionPaymentRequest::ContactFields convert(const PaymentOptions& options)
{
ApplePaySessionPaymentRequest::ContactFields result;
result.email = options.requestPayerEmail;
result.name = options.requestPayerName;
result.phone = options.requestPayerPhone;
result.postalAddress = options.requestShipping;
return result;
}
static ApplePaySessionPaymentRequest::ShippingType convert(PaymentShippingType type)
{
switch (type) {
case PaymentShippingType::Shipping:
return ApplePaySessionPaymentRequest::ShippingType::Shipping;
case PaymentShippingType::Delivery:
return ApplePaySessionPaymentRequest::ShippingType::Delivery;
case PaymentShippingType::Pickup:
return ApplePaySessionPaymentRequest::ShippingType::StorePickup;
}
ASSERT_NOT_REACHED();
return ApplePaySessionPaymentRequest::ShippingType::Shipping;
}
static ExceptionOr<ApplePaySessionPaymentRequest::ShippingMethod> convertAndValidate(const PaymentShippingOption& shippingOption, const String& expectedCurrency)
{
auto exception = validate(shippingOption.amount, expectedCurrency);
if (exception.hasException())
return exception.releaseException();
ApplePaySessionPaymentRequest::ShippingMethod result;
result.amount = shippingOption.amount.value;
result.label = shippingOption.label;
result.identifier = shippingOption.id;
return { WTFMove(result) };
}
ExceptionOr<void> ApplePayPaymentHandler::convertData(JSC::JSValue&& data)
{
auto& context = *scriptExecutionContext();
auto throwScope = DECLARE_THROW_SCOPE(context.vm());
auto applePayRequest = convertDictionary<ApplePayRequest>(*context.execState(), WTFMove(data));
if (throwScope.exception())
return Exception { ExistingExceptionError };
m_applePayRequest = WTFMove(applePayRequest);
return { };
}
ExceptionOr<void> ApplePayPaymentHandler::show()
{
auto validatedRequest = convertAndValidate(m_applePayRequest->version, *m_applePayRequest, paymentCoordinator());
if (validatedRequest.hasException())
return validatedRequest.releaseException();
ApplePaySessionPaymentRequest request = validatedRequest.releaseReturnValue();
request.setRequester(ApplePaySessionPaymentRequest::Requester::PaymentRequest);
String expectedCurrency = m_paymentRequest->paymentDetails().total.amount.currency;
request.setCurrencyCode(expectedCurrency);
auto total = convertAndValidate(m_paymentRequest->paymentDetails().total, expectedCurrency);
ASSERT(!total.hasException());
request.setTotal(total.releaseReturnValue());
auto convertedLineItems = convertAndValidate(m_paymentRequest->paymentDetails().displayItems, expectedCurrency);
if (convertedLineItems.hasException())
return convertedLineItems.releaseException();
request.setLineItems(convertedLineItems.releaseReturnValue());
request.setRequiredShippingContactFields(convert(m_paymentRequest->paymentOptions()));
if (m_paymentRequest->paymentOptions().requestShipping)
request.setShippingType(convert(m_paymentRequest->paymentOptions().shippingType));
Vector<ApplePaySessionPaymentRequest::ShippingMethod> shippingMethods;
shippingMethods.reserveInitialCapacity(m_paymentRequest->paymentDetails().shippingOptions.size());
for (auto& shippingOption : m_paymentRequest->paymentDetails().shippingOptions) {
auto convertedShippingOption = convertAndValidate(shippingOption, expectedCurrency);
if (convertedShippingOption.hasException())
return convertedShippingOption.releaseException();
shippingMethods.uncheckedAppend(convertedShippingOption.releaseReturnValue());
}
request.setShippingMethods(shippingMethods);
auto exception = PaymentRequestValidator::validate(request);
if (exception.hasException())
return exception.releaseException();
Vector<URL> linkIconURLs;
for (auto& icon : LinkIconCollector { document() }.iconsOfTypes({ LinkIconType::TouchIcon, LinkIconType::TouchPrecomposedIcon }))
linkIconURLs.append(icon.url);
paymentCoordinator().beginPaymentSession(*this, document().url(), linkIconURLs, request);
return { };
}
void ApplePayPaymentHandler::hide()
{
paymentCoordinator().abortPaymentSession();
}
static bool shouldDiscloseApplePayCapability(Document& document)
{
auto* page = document.page();
if (!page || page->usesEphemeralSession())
return false;
return document.settings().applePayCapabilityDisclosureAllowed();
}
void ApplePayPaymentHandler::canMakePayment(Function<void(bool)>&& completionHandler)
{
if (!shouldDiscloseApplePayCapability(document())) {
completionHandler(paymentCoordinator().canMakePayments());
return;
}
paymentCoordinator().canMakePaymentsWithActiveCard(m_applePayRequest->merchantIdentifier, document().domain(), WTFMove(completionHandler));
}
ExceptionOr<ApplePaySessionPaymentRequest::TotalAndLineItems> ApplePayPaymentHandler::computeTotalAndLineItems()
{
auto& details = m_paymentRequest->paymentDetails();
String currency = details.total.amount.currency;
auto convertedTotal = convertAndValidate(details.total, currency);
if (convertedTotal.hasException())
return convertedTotal.releaseException();
auto total = convertedTotal.releaseReturnValue();
auto convertedLineItems = convertAndValidate(details.displayItems, currency);
if (convertedLineItems.hasException())
return convertedLineItems.releaseException();
auto lineItems = convertedLineItems.releaseReturnValue();
if (!m_selectedPaymentMethodType)
return ApplePaySessionPaymentRequest::TotalAndLineItems { WTFMove(total), WTFMove(lineItems) };
auto& modifiers = details.modifiers;
auto& serializedModifierData = m_paymentRequest->serializedModifierData();
ASSERT(modifiers.size() == serializedModifierData.size());
for (size_t i = 0; i < modifiers.size(); ++i) {
auto convertedIdentifier = convertAndValidatePaymentMethodIdentifier(modifiers[i].supportedMethods);
if (!convertedIdentifier || !handlesIdentifier(*convertedIdentifier))
continue;
if (serializedModifierData[i].isEmpty())
continue;
auto& execState = *document().execState();
auto scope = DECLARE_THROW_SCOPE(execState.vm());
JSC::JSValue data;
{
auto lock = JSC::JSLockHolder { &execState };
data = JSONParse(&execState, serializedModifierData[i]);
if (scope.exception())
return Exception { ExistingExceptionError };
}
auto applePayModifier = convertDictionary<ApplePayModifier>(execState, WTFMove(data));
if (scope.exception())
return Exception { ExistingExceptionError };
if (applePayModifier.paymentMethodType != *m_selectedPaymentMethodType)
continue;
if (modifiers[i].total) {
auto totalOverride = convertAndValidate(*modifiers[i].total, currency);
if (totalOverride.hasException())
return totalOverride.releaseException();
total = totalOverride.releaseReturnValue();
}
auto additionalDisplayItems = convertAndValidate(modifiers[i].additionalDisplayItems, currency);
if (additionalDisplayItems.hasException())
return additionalDisplayItems.releaseException();
lineItems.appendVector(additionalDisplayItems.releaseReturnValue());
break;
}
return ApplePaySessionPaymentRequest::TotalAndLineItems { WTFMove(total), WTFMove(lineItems) };
}
ExceptionOr<void> ApplePayPaymentHandler::detailsUpdated(PaymentRequest::UpdateReason reason, const String& error)
{
using Reason = PaymentRequest::UpdateReason;
switch (reason) {
case Reason::ShowDetailsResolved:
return { };
case Reason::ShippingAddressChanged:
return shippingAddressUpdated(error);
case Reason::ShippingOptionChanged:
return shippingOptionUpdated();
case Reason::PaymentMethodChanged:
return paymentMethodUpdated();
}
ASSERT_NOT_REACHED();
return { };
}
ExceptionOr<void> ApplePayPaymentHandler::merchantValidationCompleted(JSC::JSValue&& merchantSessionValue)
{
if (!paymentCoordinator().hasActiveSession())
return Exception { InvalidStateError };
if (!merchantSessionValue.isObject())
return Exception { TypeError };
String errorMessage;
auto merchantSession = PaymentMerchantSession::fromJS(*document().execState(), asObject(merchantSessionValue), errorMessage);
if (!merchantSession)
return Exception { TypeError, WTFMove(errorMessage) };
paymentCoordinator().completeMerchantValidation(*merchantSession);
return { };
}
ExceptionOr<void> ApplePayPaymentHandler::shippingAddressUpdated(const String& error)
{
ASSERT(m_isUpdating);
m_isUpdating = false;
ShippingContactUpdate update;
if (m_paymentRequest->paymentOptions().requestShipping && m_paymentRequest->paymentDetails().shippingOptions.isEmpty()) {
PaymentError paymentError;
paymentError.code = PaymentError::Code::ShippingContactInvalid;
paymentError.message = error;
update.errors.append(WTFMove(paymentError));
}
auto newTotalAndLineItems = computeTotalAndLineItems();
if (newTotalAndLineItems.hasException())
return newTotalAndLineItems.releaseException();
update.newTotalAndLineItems = newTotalAndLineItems.releaseReturnValue();
paymentCoordinator().completeShippingContactSelection(WTFMove(update));
return { };
}
ExceptionOr<void> ApplePayPaymentHandler::shippingOptionUpdated()
{
ASSERT(m_isUpdating);
m_isUpdating = false;
ShippingMethodUpdate update;
auto newTotalAndLineItems = computeTotalAndLineItems();
if (newTotalAndLineItems.hasException())
return newTotalAndLineItems.releaseException();
update.newTotalAndLineItems = newTotalAndLineItems.releaseReturnValue();
paymentCoordinator().completeShippingMethodSelection(WTFMove(update));
return { };
}
ExceptionOr<void> ApplePayPaymentHandler::paymentMethodUpdated()
{
ASSERT(m_isUpdating);
m_isUpdating = false;
PaymentMethodUpdate update;
auto newTotalAndLineItems = computeTotalAndLineItems();
if (newTotalAndLineItems.hasException())
return newTotalAndLineItems.releaseException();
update.newTotalAndLineItems = newTotalAndLineItems.releaseReturnValue();
paymentCoordinator().completePaymentMethodSelection(WTFMove(update));
return { };
}
void ApplePayPaymentHandler::complete(std::optional<PaymentComplete>&& result)
{
if (!result) {
paymentCoordinator().completePaymentSession(std::nullopt);
return;
}
PaymentAuthorizationResult authorizationResult;
switch (*result) {
case PaymentComplete::Fail:
case PaymentComplete::Unknown:
authorizationResult.status = PaymentAuthorizationStatus::Failure;
break;
case PaymentComplete::Success:
authorizationResult.status = PaymentAuthorizationStatus::Success;
break;
}
paymentCoordinator().completePaymentSession(WTFMove(authorizationResult));
}
unsigned ApplePayPaymentHandler::version() const
{
if (!m_applePayRequest)
return 0;
auto version = m_applePayRequest->version;
ASSERT(paymentCoordinator().supportsVersion(version));
return version;
}
void ApplePayPaymentHandler::validateMerchant(const URL& validationURL)
{
if (validationURL.isValid())
m_paymentRequest->dispatchEvent(MerchantValidationEvent::create(eventNames().merchantvalidationEvent, validationURL, m_paymentRequest.get()).get());
}
static Ref<PaymentAddress> convert(const ApplePayPaymentContact& contact)
{
return PaymentAddress::create(contact.countryCode, contact.addressLines.value_or(Vector<String>()), contact.administrativeArea, contact.locality, contact.subLocality, contact.postalCode, String(), String(), String(), contact.localizedName, contact.phoneNumber);
}
void ApplePayPaymentHandler::didAuthorizePayment(const Payment& payment)
{
ASSERT(!m_isUpdating);
auto applePayPayment = payment.toApplePayPayment(version());
auto& execState = *document().execState();
auto lock = JSC::JSLockHolder { &execState };
auto details = JSC::Strong<JSC::JSObject> { execState.vm(), asObject(toJS<IDLDictionary<ApplePayPayment>>(execState, *JSC::jsCast<JSDOMGlobalObject*>(execState.lexicalGlobalObject()), applePayPayment)) };
const auto& shippingContact = applePayPayment.shippingContact.value_or(ApplePayPaymentContact());
m_paymentRequest->accept(WTF::get<URL>(m_identifier).string(), WTFMove(details), convert(shippingContact), shippingContact.localizedName, shippingContact.emailAddress, shippingContact.phoneNumber);
}
void ApplePayPaymentHandler::didSelectShippingMethod(const ApplePaySessionPaymentRequest::ShippingMethod& shippingMethod)
{
ASSERT(!m_isUpdating);
m_isUpdating = true;
m_paymentRequest->shippingOptionChanged(shippingMethod.identifier);
}
void ApplePayPaymentHandler::didSelectShippingContact(const PaymentContact& shippingContact)
{
ASSERT(!m_isUpdating);
m_isUpdating = true;
m_paymentRequest->shippingAddressChanged(convert(shippingContact.toApplePayPaymentContact(version())));
}
void ApplePayPaymentHandler::didSelectPaymentMethod(const PaymentMethod& paymentMethod)
{
ASSERT(!m_isUpdating);
m_isUpdating = true;
m_selectedPaymentMethodType = paymentMethod.toApplePayPaymentMethod().type;
m_paymentRequest->paymentMethodChanged();
}
void ApplePayPaymentHandler::didCancelPaymentSession()
{
m_paymentRequest->cancel();
}
} // namespace WebCore
#endif // ENABLE(APPLE_PAY) && ENABLE(PAYMENT_REQUEST)