/*
 * Copyright (C) 2015-2019 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 "ApplePayCancelEvent.h"
#include "ApplePayCouponCodeChangedEvent.h"
#include "ApplePayCouponCodeUpdate.h"
#include "ApplePayErrorCode.h"
#include "ApplePayErrorContactField.h"
#include "ApplePayLineItem.h"
#include "ApplePayPaymentAuthorizationResult.h"
#include "ApplePayPaymentAuthorizedEvent.h"
#include "ApplePayPaymentMethodSelectedEvent.h"
#include "ApplePayPaymentMethodUpdate.h"
#include "ApplePayPaymentRequest.h"
#include "ApplePayShippingContactSelectedEvent.h"
#include "ApplePayShippingContactUpdate.h"
#include "ApplePayShippingMethod.h"
#include "ApplePayShippingMethodSelectedEvent.h"
#include "ApplePayShippingMethodUpdate.h"
#include "ApplePayValidateMerchantEvent.h"
#include "DOMWindow.h"
#include "Document.h"
#include "DocumentLoader.h"
#include "EventNames.h"
#include "Frame.h"
#include "JSDOMPromiseDeferred.h"
#include "LinkIconCollector.h"
#include "LinkIconType.h"
#include "Page.h"
#include "PageConsoleClient.h"
#include "PaymentContact.h"
#include "PaymentCoordinator.h"
#include "PaymentMerchantSession.h"
#include "PaymentMethod.h"
#include "PaymentRequestUtilities.h"
#include "PaymentRequestValidator.h"
#include "SecurityOrigin.h"
#include "Settings.h"
#include "UserGestureIndicator.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/RunLoop.h>

namespace WebCore {

WTF_MAKE_ISO_ALLOCATED_IMPL(ApplePaySession);

static ExceptionOr<ApplePayLineItem> convertAndValidateTotal(ApplePayLineItem&& lineItem)
{
    if (!isValidDecimalMonetaryValue(lineItem.amount))
        return Exception { TypeError, makeString("\"" + lineItem.amount, "\" is not a valid amount.") };

    auto validatedTotal = PaymentRequestValidator::validateTotal(lineItem);
    if (validatedTotal.hasException())
        return validatedTotal.releaseException();

    return WTFMove(lineItem);
}

static ExceptionOr<ApplePayLineItem> convertAndValidate(ApplePayLineItem&& lineItem)
{
    if (lineItem.type == ApplePayLineItem::Type::Pending) {
        // It is OK for pending types to not have an amount.
        lineItem.amount = nullString();
    } else {
        if (!isValidDecimalMonetaryValue(lineItem.amount))
            return Exception { TypeError, makeString("\"" + lineItem.amount, "\" is not a valid amount.") };
    }

    return WTFMove(lineItem);
}

static ExceptionOr<Vector<ApplePayLineItem>> convertAndValidate(std::optional<Vector<ApplePayLineItem>>&& lineItems)
{
    Vector<ApplePayLineItem> result;
    if (!lineItems)
        return WTFMove(result);

    result.reserveInitialCapacity(lineItems->size());
    
    for (auto lineItem : lineItems.value()) {
        auto convertedLineItem = convertAndValidate(WTFMove(lineItem));
        if (convertedLineItem.hasException())
            return convertedLineItem.releaseException();
        result.uncheckedAppend(convertedLineItem.releaseReturnValue());
    }

    return WTFMove(result);
}

static ExceptionOr<ApplePayShippingMethod> convertAndValidate(ApplePayShippingMethod&& shippingMethod)
{
    if (!isValidDecimalMonetaryValue(shippingMethod.amount))
        return Exception { TypeError, makeString("\"" + shippingMethod.amount, "\" is not a valid amount.") };

    return WTFMove(shippingMethod);
}

static ExceptionOr<Vector<ApplePayShippingMethod>> convertAndValidate(Vector<ApplePayShippingMethod>&& shippingMethods)
{
    Vector<ApplePayShippingMethod> result;
    result.reserveInitialCapacity(shippingMethods.size());

    for (auto& shippingMethod : shippingMethods) {
        auto convertedShippingMethod = convertAndValidate(WTFMove(shippingMethod));
        if (convertedShippingMethod.hasException())
            return convertedShippingMethod.releaseException();
        result.uncheckedAppend(convertedShippingMethod.releaseReturnValue());
    }

    return WTFMove(result);
}

#if ENABLE(APPLE_PAY_RECURRING_PAYMENTS)

static ExceptionOr<ApplePayRecurringPaymentRequest> convertAndValidate(ApplePayRecurringPaymentRequest&& recurringPaymentRequest)
{
    auto& regularBilling = recurringPaymentRequest.regularBilling;
    if (regularBilling.paymentTiming != ApplePayPaymentTiming::Recurring)
        return Exception(TypeError, "'regularBilling' must be a 'recurring' line item."_s);
    if (!regularBilling.label)
        return Exception(TypeError, "Missing label for 'regularBilling'."_s);
    if (!isValidDecimalMonetaryValue(regularBilling.amount) && regularBilling.type != ApplePayLineItem::Type::Pending)
        return Exception(TypeError, makeString('"', regularBilling.amount, "\" is not a valid amount."));

    if (auto& trialBilling = recurringPaymentRequest.trialBilling) {
        if (trialBilling->paymentTiming != ApplePayPaymentTiming::Recurring)
            return Exception(TypeError, "'trialBilling' must be a 'recurring' line item."_s);
        if (!trialBilling->label)
            return Exception(TypeError, "Missing label for 'trialBilling'."_s);
        if (!isValidDecimalMonetaryValue(trialBilling->amount) && trialBilling->type != ApplePayLineItem::Type::Pending)
            return Exception(TypeError, makeString('"', trialBilling->amount, "\" is not a valid amount."));
    }

    if (auto& managementURL = recurringPaymentRequest.managementURL; !URL { managementURL }.isValid())
        return Exception(TypeError, makeString('"', managementURL, "\" is not a valid URL."));

    if (auto& tokenNotificationURL = recurringPaymentRequest.tokenNotificationURL; !tokenNotificationURL.isNull() && !URL { tokenNotificationURL }.isValid())
        return Exception(TypeError, makeString('"', tokenNotificationURL, "\" is not a valid URL."));

    return WTFMove(recurringPaymentRequest);
}

#endif // ENABLE(APPLE_PAY_RECURRING_PAYMENTS)

#if ENABLE(APPLE_PAY_AUTOMATIC_RELOAD_PAYMENTS)

static ExceptionOr<ApplePayAutomaticReloadPaymentRequest> convertAndValidate(ApplePayAutomaticReloadPaymentRequest&& automaticReloadPaymentRequest)
{
    auto& automaticReloadBilling = automaticReloadPaymentRequest.automaticReloadBilling;
    if (automaticReloadBilling.paymentTiming != ApplePayPaymentTiming::AutomaticReload)
        return Exception(TypeError, "'automaticReloadBilling' must be an 'automaticReload' line item."_s);
    if (!automaticReloadBilling.label)
        return Exception(TypeError, "Missing label for 'automaticReloadBilling'."_s);
    if (!isValidDecimalMonetaryValue(automaticReloadBilling.amount) && automaticReloadBilling.type != ApplePayLineItem::Type::Pending)
        return Exception(TypeError, makeString('"', automaticReloadBilling.amount, "\" is not a valid amount."));
    if (!isValidDecimalMonetaryValue(automaticReloadBilling.automaticReloadPaymentThresholdAmount))
        return Exception(TypeError, makeString('"', automaticReloadBilling.automaticReloadPaymentThresholdAmount, "\" is not a valid automaticReloadPaymentThresholdAmount."));

    if (auto& managementURL = automaticReloadPaymentRequest.managementURL; !URL { managementURL }.isValid())
        return Exception(TypeError, makeString('"', managementURL, "\" is not a valid URL."));

    if (auto& tokenNotificationURL = automaticReloadPaymentRequest.tokenNotificationURL; !tokenNotificationURL.isNull() && !URL { tokenNotificationURL }.isValid())
        return Exception(TypeError, makeString('"', tokenNotificationURL, "\" is not a valid URL."));

    return WTFMove(automaticReloadPaymentRequest);
}

#endif // ENABLE(APPLE_PAY_AUTOMATIC_RELOAD_PAYMENTS)

#if ENABLE(APPLE_PAY_MULTI_MERCHANT_PAYMENTS)

static ExceptionOr<ApplePayPaymentTokenContext> convertAndValidate(ApplePayPaymentTokenContext&& tokenContext)
{
    if (!isValidDecimalMonetaryValue(tokenContext.amount))
        return Exception { TypeError, makeString("\"" + tokenContext.amount, "\" is not a valid amount.") };

    return WTFMove(tokenContext);
}

static ExceptionOr<Vector<ApplePayPaymentTokenContext>> convertAndValidate(Vector<ApplePayPaymentTokenContext>&& tokenContexts)
{
    Vector<ApplePayPaymentTokenContext> result;
    result.reserveInitialCapacity(tokenContexts.size());

    for (auto& tokenContext : tokenContexts) {
        auto convertedTokenContext = convertAndValidate(WTFMove(tokenContext));
        if (convertedTokenContext.hasException())
            return convertedTokenContext.releaseException();
        result.uncheckedAppend(convertedTokenContext.releaseReturnValue());
    }

    return WTFMove(result);
}

#endif // ENABLE(APPLE_PAY_MULTI_MERCHANT_PAYMENTS)

static ExceptionOr<ApplePaySessionPaymentRequest> convertAndValidate(Document& document, unsigned version, ApplePayPaymentRequest&& paymentRequest, const PaymentCoordinator& paymentCoordinator)
{
    auto convertedRequest = convertAndValidate(document, version, paymentRequest, paymentCoordinator);
    if (convertedRequest.hasException())
        return convertedRequest.releaseException();

    auto result = convertedRequest.releaseReturnValue();
    result.setRequester(ApplePaySessionPaymentRequest::Requester::ApplePayJS);
    result.setCurrencyCode(paymentRequest.currencyCode);

    auto total = convertAndValidateTotal(WTFMove(paymentRequest.total));
    if (total.hasException())
        return total.releaseException();
    result.setTotal(total.releaseReturnValue());

    auto lineItems = convertAndValidate(WTFMove(paymentRequest.lineItems));
    if (lineItems.hasException())
        return lineItems.releaseException();
    result.setLineItems(lineItems.releaseReturnValue());

    result.setShippingType(paymentRequest.shippingType);

    if (paymentRequest.shippingMethods) {
        auto shippingMethods = convertAndValidate(WTFMove(*paymentRequest.shippingMethods));
        if (shippingMethods.hasException())
            return shippingMethods.releaseException();
        result.setShippingMethods(shippingMethods.releaseReturnValue());
    }

#if ENABLE(APPLE_PAY_RECURRING_PAYMENTS)
    if (paymentRequest.recurringPaymentRequest) {
        auto recurringPaymentRequest = convertAndValidate(WTFMove(*paymentRequest.recurringPaymentRequest));
        if (recurringPaymentRequest.hasException())
            return recurringPaymentRequest.releaseException();
        result.setRecurringPaymentRequest(recurringPaymentRequest.releaseReturnValue());
    }
#endif

#if ENABLE(APPLE_PAY_AUTOMATIC_RELOAD_PAYMENTS)
    if (paymentRequest.automaticReloadPaymentRequest) {
        auto automaticReloadPaymentRequest = convertAndValidate(WTFMove(*paymentRequest.automaticReloadPaymentRequest));
        if (automaticReloadPaymentRequest.hasException())
            return automaticReloadPaymentRequest.releaseException();
        result.setAutomaticReloadPaymentRequest(automaticReloadPaymentRequest.releaseReturnValue());
    }
#endif

#if ENABLE(APPLE_PAY_MULTI_MERCHANT_PAYMENTS)
    if (paymentRequest.multiTokenContexts) {
        auto multiTokenContexts = convertAndValidate(WTFMove(*paymentRequest.multiTokenContexts));
        if (multiTokenContexts.hasException())
            return multiTokenContexts.releaseException();
        result.setMultiTokenContexts(multiTokenContexts.releaseReturnValue());
    }
#endif

    // FIXME: Merge this validation into the validation we are doing above.
    constexpr OptionSet fieldsToValidate = {
        PaymentRequestValidator::Field::MerchantCapabilities,
        PaymentRequestValidator::Field::SupportedNetworks,
        PaymentRequestValidator::Field::CountryCode,
        PaymentRequestValidator::Field::CurrencyCode,
        PaymentRequestValidator::Field::Total,
        PaymentRequestValidator::Field::ShippingMethods,
    };
    auto validatedPaymentRequest = PaymentRequestValidator::validate(result, fieldsToValidate);
    if (validatedPaymentRequest.hasException())
        return validatedPaymentRequest.releaseException();

    return WTFMove(result);
}

#if HAVE(APPLE_PAY_PAYMENT_ORDER_DETAILS)

static ExceptionOr<ApplePayPaymentOrderDetails> convertAndValidate(ApplePayPaymentOrderDetails&& orderDetails)
{
    if (auto& webServiceURL = orderDetails.webServiceURL; !URL { webServiceURL }.isValid())
        return Exception(TypeError, makeString('"', webServiceURL, "\" is not a valid URL."));

    return WTFMove(orderDetails);
}

#endif // HAVE(APPLE_PAY_PAYMENT_ORDER_DETAILS)

static ExceptionOr<ApplePayPaymentAuthorizationResult> convertAndValidate(ApplePayPaymentAuthorizationResult&& result)
{
    switch (result.status) {
    case ApplePayPaymentAuthorizationResult::Success:
    case ApplePayPaymentAuthorizationResult::Failure:
    case ApplePayPaymentAuthorizationResult::PINRequired:
    case ApplePayPaymentAuthorizationResult::PINIncorrect:
    case ApplePayPaymentAuthorizationResult::PINLockout:
        break;

    case ApplePayPaymentAuthorizationResult::InvalidBillingPostalAddress:
        result.status = ApplePayPaymentAuthorizationResult::Failure;
        result.errors.insert(0, ApplePayError::create(ApplePayErrorCode::BillingContactInvalid, std::nullopt, nullString()));
        break;

    case ApplePayPaymentAuthorizationResult::InvalidShippingPostalAddress:
        result.status = ApplePayPaymentAuthorizationResult::Failure;
        result.errors.insert(0, ApplePayError::create(ApplePayErrorCode::ShippingContactInvalid, ApplePayErrorContactField::PostalAddress, nullString()));
        break;

    case ApplePayPaymentAuthorizationResult::InvalidShippingContact:
        result.status = ApplePayPaymentAuthorizationResult::Failure;
        result.errors.insert(0, ApplePayError::create(ApplePayErrorCode::ShippingContactInvalid, std::nullopt, nullString()));
        break;

    default:
        return Exception { InvalidAccessError };
    }

#if HAVE(APPLE_PAY_PAYMENT_ORDER_DETAILS)
    if (auto orderDetails = WTFMove(result.orderDetails)) {
        auto convertedOrderDetails = convertAndValidate(WTFMove(*orderDetails));
        if (convertedOrderDetails.hasException())
            return convertedOrderDetails.releaseException();
        result.orderDetails = convertedOrderDetails.releaseReturnValue();
    }
#endif

    return WTFMove(result);
}

static ExceptionOr<ApplePayPaymentMethodUpdate> convertAndValidate(ApplePayPaymentMethodUpdate&& update)
{
    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    update.newTotal = convertedNewTotal.releaseReturnValue();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();
    update.newLineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(update);
}

static ExceptionOr<ApplePayShippingContactUpdate> convertAndValidate(ApplePayShippingContactUpdate&& update)
{
    auto convertedNewShippingMethods = convertAndValidate(WTFMove(update.newShippingMethods));
    if (convertedNewShippingMethods.hasException())
        return convertedNewShippingMethods.releaseException();
    update.newShippingMethods = convertedNewShippingMethods.releaseReturnValue();

    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    update.newTotal = convertedNewTotal.releaseReturnValue();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();
    update.newLineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(update);
}

static ExceptionOr<ApplePayShippingMethodUpdate> convertAndValidate(ApplePayShippingMethodUpdate&& update)
{
    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    update.newTotal = convertedNewTotal.releaseReturnValue();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();
    update.newLineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(update);
}

#if ENABLE(APPLE_PAY_COUPON_CODE)

static ExceptionOr<ApplePayCouponCodeUpdate> convertAndValidate(ApplePayCouponCodeUpdate&& update)
{
    auto convertedNewShippingMethods = convertAndValidate(WTFMove(update.newShippingMethods));
    if (convertedNewShippingMethods.hasException())
        return convertedNewShippingMethods.releaseException();
    update.newShippingMethods = convertedNewShippingMethods.releaseReturnValue();

    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    update.newTotal = convertedNewTotal.releaseReturnValue();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();
    update.newLineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(update);
}

#endif // ENABLE(APPLE_PAY_COUPON_CODE)

ExceptionOr<Ref<ApplePaySession>> ApplePaySession::create(Document& document, unsigned version, ApplePayPaymentRequest&& paymentRequest)
{
    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    if (!UserGestureIndicator::processingUserGesture())
        return Exception { InvalidAccessError, "Must create a new ApplePaySession from a user gesture handler."_s };

    if (!document.page())
        return Exception { InvalidAccessError, "Frame is detached"_s };

    auto convertedPaymentRequest = convertAndValidate(document, version, WTFMove(paymentRequest), document.page()->paymentCoordinator());
    if (convertedPaymentRequest.hasException())
        return convertedPaymentRequest.releaseException();

    auto session = adoptRef(*new ApplePaySession(document, version, convertedPaymentRequest.releaseReturnValue()));
    session->suspendIfNeeded();
    return session;
}

ApplePaySession::ApplePaySession(Document& document, unsigned version, ApplePaySessionPaymentRequest&& paymentRequest)
    : ActiveDOMObject { document }
    , m_paymentRequest { WTFMove(paymentRequest) }
    , m_version { version }
{
    ASSERT(document.page()->paymentCoordinator().supportsVersion(document, version));
}

ApplePaySession::~ApplePaySession() = default;

ExceptionOr<bool> ApplePaySession::supportsVersion(Document& document, unsigned version)
{
    if (!version)
        return Exception { InvalidAccessError };

    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    auto* page = document.page();
    if (!page)
        return Exception { InvalidAccessError };

    return page->paymentCoordinator().supportsVersion(document, version);
}

static bool shouldDiscloseApplePayCapability(Document& document)
{
    auto* page = document.page();
    if (!page || page->usesEphemeralSession())
        return false;

    return document.settings().applePayCapabilityDisclosureAllowed();
}

ExceptionOr<bool> ApplePaySession::canMakePayments(Document& document)
{
    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    auto* page = document.page();
    if (!page)
        return Exception { InvalidAccessError };

    return page->paymentCoordinator().canMakePayments();
}

ExceptionOr<void> ApplePaySession::canMakePaymentsWithActiveCard(Document& document, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise)
{
    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    RefPtr<DeferredPromise> promise(WTFMove(passedPromise));
    if (!shouldDiscloseApplePayCapability(document)) {
        auto* page = document.page();
        if (!page)
            return Exception { InvalidAccessError };

        auto& paymentCoordinator = page->paymentCoordinator();
        bool canMakePayments = paymentCoordinator.canMakePayments();

        RunLoop::main().dispatch([promise, canMakePayments]() mutable {
            promise->resolve<IDLBoolean>(canMakePayments);
        });
        return { };
    }

    auto* page = document.page();
    if (!page)
        return Exception { InvalidAccessError };

    auto& paymentCoordinator = page->paymentCoordinator();

    paymentCoordinator.canMakePaymentsWithActiveCard(document, merchantIdentifier, [promise](bool canMakePayments) mutable {
        promise->resolve<IDLBoolean>(canMakePayments);
    });
    return { };
}

ExceptionOr<void> ApplePaySession::openPaymentSetup(Document& document, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise)
{
    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    if (!UserGestureIndicator::processingUserGesture())
        return Exception { InvalidAccessError, "Must call ApplePaySession.openPaymentSetup from a user gesture handler."_s };

    auto* page = document.page();
    if (!page)
        return Exception { InvalidAccessError };

    auto& paymentCoordinator = page->paymentCoordinator();

    RefPtr<DeferredPromise> promise(WTFMove(passedPromise));
    paymentCoordinator.openPaymentSetup(document, merchantIdentifier, [promise](bool result) mutable {
        promise->resolve<IDLBoolean>(result);
    });

    return { };
}

ExceptionOr<void> ApplePaySession::begin(Document& document)
{
    if (!canBegin())
        return Exception { InvalidAccessError, "Payment session is already active."_s };

    if (paymentCoordinator().hasActiveSession())
        return Exception { InvalidAccessError, "Page already has an active payment session."_s };

    if (!paymentCoordinator().beginPaymentSession(document, *this, m_paymentRequest))
        return Exception { InvalidAccessError, "There is already has an active payment session."_s };

    m_state = State::Active;

    return { };
}

ExceptionOr<void> ApplePaySession::abort()
{
    if (!canAbort())
        return Exception { InvalidAccessError };

    m_state = State::Aborted;
    paymentCoordinator().abortPaymentSession();

    return { };
}

ExceptionOr<void> ApplePaySession::completeMerchantValidation(JSC::JSGlobalObject& state, JSC::JSValue merchantSessionValue)
{
    if (!canCompleteMerchantValidation())
        return Exception { InvalidAccessError };

    if (!merchantSessionValue.isObject())
        return Exception { TypeError };

    auto& document = *downcast<Document>(scriptExecutionContext());
    auto& window = *document.domWindow();

    String errorMessage;
    auto merchantSession = PaymentMerchantSession::fromJS(state, asObject(merchantSessionValue), errorMessage);
    if (!merchantSession) {
        window.printErrorMessage(errorMessage);
        return Exception { InvalidAccessError };
    }

    m_merchantValidationState = MerchantValidationState::ValidationComplete;
    paymentCoordinator().completeMerchantValidation(*merchantSession);

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingMethodSelection(ApplePayShippingMethodUpdate&& update)
{
    if (!canCompleteShippingMethodSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completeShippingMethodSelection(convertedUpdate.releaseReturnValue());

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingContactSelection(ApplePayShippingContactUpdate&& update)
{
    if (!canCompleteShippingContactSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completeShippingContactSelection(convertedUpdate.releaseReturnValue());

    return { };
}

ExceptionOr<void> ApplePaySession::completePaymentMethodSelection(ApplePayPaymentMethodUpdate&& update)
{
    if (!canCompletePaymentMethodSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completePaymentMethodSelection(convertedUpdate.releaseReturnValue());

    return { };
}

#if ENABLE(APPLE_PAY_COUPON_CODE)

ExceptionOr<void> ApplePaySession::completeCouponCodeChange(ApplePayCouponCodeUpdate&& update)
{
    if (!canCompleteCouponCodeChange())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completeCouponCodeChange(convertedUpdate.releaseReturnValue());

    return { };
}

#endif // ENABLE(APPLE_PAY_COUPON_CODE)

ExceptionOr<void> ApplePaySession::completePayment(ApplePayPaymentAuthorizationResult&& result)
{
    if (!canCompletePayment())
        return Exception { InvalidAccessError };

    auto convertedResultOrException = convertAndValidate(WTFMove(result));
    if (convertedResultOrException.hasException())
        return convertedResultOrException.releaseException();

    auto&& convertedResult = convertedResultOrException.releaseReturnValue();
    bool isFinalState = convertedResult.isFinalState();

    paymentCoordinator().completePaymentSession(WTFMove(convertedResult));

    if (!isFinalState) {
        m_state = State::Active;
        return { };
    }

    m_state = State::Completed;

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingMethodSelection(unsigned short status, ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayShippingMethodUpdate update;

    switch (status) {
    case ApplePaySession::STATUS_SUCCESS:
        break;

    case ApplePaySession::STATUS_FAILURE:
    case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS:
    case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS:
    case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT:
    case ApplePaySession::STATUS_PIN_REQUIRED:
    case ApplePaySession::STATUS_PIN_INCORRECT:
    case ApplePaySession::STATUS_PIN_LOCKOUT:
        // This is a fatal error. Cancel the request.
        m_state = State::CancelRequested;
        paymentCoordinator().cancelPaymentSession();
        return { };

    default:
        return Exception { InvalidAccessError };
    }

    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completeShippingMethodSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completeShippingContactSelection(unsigned short status, Vector<ApplePayShippingMethod>&& newShippingMethods, ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayShippingContactUpdate update;

    std::optional<ApplePayErrorCode> errorCode;
    std::optional<ApplePayErrorContactField> contactField;

    switch (status) {
    case ApplePaySession::STATUS_SUCCESS:
        break;

    case ApplePaySession::STATUS_FAILURE:
    case ApplePaySession::STATUS_PIN_REQUIRED:
    case ApplePaySession::STATUS_PIN_INCORRECT:
    case ApplePaySession::STATUS_PIN_LOCKOUT:
        errorCode = ApplePayErrorCode::Unknown;
        break;

    case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS:
        errorCode = ApplePayErrorCode::BillingContactInvalid;
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS:
        errorCode = ApplePayErrorCode::ShippingContactInvalid;
        contactField = ApplePayErrorContactField::PostalAddress;
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT:
        errorCode = ApplePayErrorCode::ShippingContactInvalid;
        break;

    default:
        return Exception { InvalidAccessError };
    }

    if (errorCode)
        update.errors = { ApplePayError::create(*errorCode, contactField, { }) };

    update.newShippingMethods = WTFMove(newShippingMethods);
    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completeShippingContactSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completePaymentMethodSelection(ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayPaymentMethodUpdate update;

    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completePaymentMethodSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completePayment(unsigned short status)
{
    ApplePayPaymentAuthorizationResult result;
    result.status = status;

    return completePayment(WTFMove(result));
}

unsigned ApplePaySession::version() const
{
    return m_version;
}

void ApplePaySession::validateMerchant(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, WTFMove(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, version(), payment);
    dispatchEvent(event.get());
}

void ApplePaySession::didSelectShippingMethod(const ApplePayShippingMethod& shippingMethod)
{
    ASSERT(m_state == State::Active);

    if (!hasEventListeners(eventNames().shippingmethodselectedEvent)) {
        paymentCoordinator().completeShippingMethodSelection({ });
        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({ });
        return;
    }

    m_state = State::ShippingContactSelected;
    auto event = ApplePayShippingContactSelectedEvent::create(eventNames().shippingcontactselectedEvent, version(), 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());
}

#if ENABLE(APPLE_PAY_COUPON_CODE)

void ApplePaySession::didChangeCouponCode(String&& couponCode)
{
    ASSERT(m_state == State::Active);

    if (!hasEventListeners(eventNames().couponcodechangedEvent)) {
        paymentCoordinator().completeCouponCodeChange({ });
        return;
    }

    m_state = State::CouponCodeChanged;
    auto event = ApplePayCouponCodeChangedEvent::create(eventNames().couponcodechangedEvent, WTFMove(couponCode));
    dispatchEvent(event.get());
}

#endif // ENABLE(APPLE_PAY_COUPON_CODE)

void ApplePaySession::didCancelPaymentSession(PaymentSessionError&& error)
{
    ASSERT(canCancel());

    m_state = State::Canceled;

    auto event = ApplePayCancelEvent::create(eventNames().cancelEvent, WTFMove(error));
    dispatchEvent(event.get());
}

const char* ApplePaySession::activeDOMObjectName() const
{
    return "ApplePaySession";
}

bool ApplePaySession::canSuspendWithoutCanceling() 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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        return false;
    }
}

void ApplePaySession::stop()
{
    if (!canAbort())
        return;

    m_state = State::Aborted;
    paymentCoordinator().abortPaymentSession();
}

void ApplePaySession::suspend(ReasonForSuspension reason)
{
    if (reason != ReasonForSuspension::BackForwardCache)
        return;

    if (canSuspendWithoutCanceling())
        return;

    auto jsWrapperProtector = makePendingActivity(*this);
    m_state = State::Canceled;
    paymentCoordinator().abortPaymentSession();
    queueTaskToDispatchEvent(*this, TaskSource::UserInteraction, ApplePayCancelEvent::create(eventNames().cancelEvent, { }));
}

PaymentCoordinator& ApplePaySession::paymentCoordinator() const
{
    return downcast<Document>(*scriptExecutionContext()).page()->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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        return false;

    case State::PaymentMethodSelected:
        return true;
    }
}

#if ENABLE(APPLE_PAY_COUPON_CODE)

bool ApplePaySession::canCompleteCouponCodeChange() 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:
    case State::CancelRequested:
    case State::PaymentMethodSelected:
        return false;

    case State::CouponCodeChanged:
        return true;
    }
}

#endif // ENABLE(APPLE_PAY_COUPON_CODE)

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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::CancelRequested:
        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:
#if ENABLE(APPLE_PAY_COUPON_CODE)
    case State::CouponCodeChanged:
#endif
    case State::Authorized:
    case State::CancelRequested:
        return false;

    case State::Completed:
    case State::Aborted:
    case State::Canceled:
        return true;
    }
}

bool ApplePaySession::virtualHasPendingActivity() const
{
    return m_state != State::Idle && !isFinalState();
}

} // namespace WebCore

#endif // ENABLE(APPLE_PAY)
