diff options
Diffstat (limited to '')
74 files changed, 14803 insertions, 0 deletions
diff --git a/dom/payments/BasicCardPayment.cpp b/dom/payments/BasicCardPayment.cpp new file mode 100644 index 0000000000..403260cd91 --- /dev/null +++ b/dom/payments/BasicCardPayment.cpp @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BasicCardPayment.h" +#include "PaymentAddress.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ErrorResult.h" +#include "nsArrayUtils.h" + +namespace mozilla::dom { +namespace { +bool IsValidNetwork(const nsAString& aNetwork) { + return aNetwork.Equals(u"amex"_ns) || aNetwork.Equals(u"cartebancaire"_ns) || + aNetwork.Equals(u"diners"_ns) || aNetwork.Equals(u"discover"_ns) || + aNetwork.Equals(u"jcb"_ns) || aNetwork.Equals(u"mastercard"_ns) || + aNetwork.Equals(u"mir"_ns) || aNetwork.Equals(u"unionpay"_ns) || + aNetwork.Equals(u"visa"_ns); +} +} // end of namespace + +StaticRefPtr<BasicCardService> gBasicCardService; + +already_AddRefed<BasicCardService> BasicCardService::GetService() { + if (!gBasicCardService) { + gBasicCardService = new BasicCardService(); + ClearOnShutdown(&gBasicCardService); + } + RefPtr<BasicCardService> service = gBasicCardService; + return service.forget(); +} + +bool BasicCardService::IsBasicCardPayment(const nsAString& aSupportedMethods) { + return aSupportedMethods.Equals(u"basic-card"_ns); +} + +bool BasicCardService::IsValidBasicCardRequest(JSContext* aCx, JSObject* aData, + nsAString& aErrorMsg) { + if (!aData) { + return true; + } + JS::Rooted<JS::Value> data(aCx, JS::ObjectValue(*aData)); + + BasicCardRequest request; + if (!request.Init(aCx, data)) { + aErrorMsg.AssignLiteral( + "Fail to convert methodData.data to BasicCardRequest."); + return false; + } + + for (const nsString& network : request.mSupportedNetworks) { + if (!IsValidNetwork(network)) { + aErrorMsg.Assign(network + u" is not an valid network."_ns); + return false; + } + } + return true; +} + +bool BasicCardService::IsValidExpiryMonth(const nsAString& aExpiryMonth) { + // ExpiryMonth can only be + // 1. empty string + // 2. 01 ~ 12 + if (aExpiryMonth.IsEmpty()) { + return true; + } + if (aExpiryMonth.Length() != 2) { + return false; + } + // can only be 00 ~ 09 + if (aExpiryMonth.CharAt(0) == '0') { + if (aExpiryMonth.CharAt(1) < '0' || aExpiryMonth.CharAt(1) > '9') { + return false; + } + return true; + } + // can only be 11 or 12 + if (aExpiryMonth.CharAt(0) == '1') { + if (aExpiryMonth.CharAt(1) != '1' && aExpiryMonth.CharAt(1) != '2') { + return false; + } + return true; + } + return false; +} + +bool BasicCardService::IsValidExpiryYear(const nsAString& aExpiryYear) { + // ExpiryYear can only be + // 1. empty string + // 2. 0000 ~ 9999 + if (!aExpiryYear.IsEmpty()) { + if (aExpiryYear.Length() != 4) { + return false; + } + for (uint32_t index = 0; index < 4; ++index) { + if (aExpiryYear.CharAt(index) < '0' || aExpiryYear.CharAt(index) > '9') { + return false; + } + } + } + return true; +} + +void BasicCardService::CheckForValidBasicCardErrors(JSContext* aCx, + JSObject* aData, + ErrorResult& aRv) { + MOZ_ASSERT(aData, "Don't pass null data"); + JS::Rooted<JS::Value> data(aCx, JS::ObjectValue(*aData)); + + // XXXbz Just because aData converts to BasicCardErrors right now doesn't mean + // it will if someone tries again! Should we be replacing aData with a + // conversion of the BasicCardErrors dictionary to a JS object in a clean + // compartment or something? + BasicCardErrors bcError; + if (!bcError.Init(aCx, data)) { + aRv.NoteJSContextException(aCx); + } +} +} // namespace mozilla::dom diff --git a/dom/payments/BasicCardPayment.h b/dom/payments/BasicCardPayment.h new file mode 100644 index 0000000000..e7575f2024 --- /dev/null +++ b/dom/payments/BasicCardPayment.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BasicCardPayment_h +#define mozilla_dom_BasicCardPayment_h + +#include "mozilla/dom/BasicCardPaymentBinding.h" +#include "nsPIDOMWindow.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class BasicCardService final { + public: + NS_INLINE_DECL_REFCOUNTING(BasicCardService) + + static already_AddRefed<BasicCardService> GetService(); + + bool IsBasicCardPayment(const nsAString& aSupportedMethods); + bool IsValidBasicCardRequest(JSContext* aCx, JSObject* aData, + nsAString& aErrorMsg); + void CheckForValidBasicCardErrors(JSContext* aCx, JSObject* aData, + ErrorResult& aRv); + bool IsValidExpiryMonth(const nsAString& aExpiryMonth); + bool IsValidExpiryYear(const nsAString& aExpiryYear); + + private: + BasicCardService() = default; + ~BasicCardService() = default; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/MerchantValidationEvent.cpp b/dom/payments/MerchantValidationEvent.cpp new file mode 100644 index 0000000000..cd1c8f1800 --- /dev/null +++ b/dom/payments/MerchantValidationEvent.cpp @@ -0,0 +1,190 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/MerchantValidationEvent.h" +#include "nsNetCID.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/PaymentRequest.h" +#include "mozilla/dom/Location.h" +#include "mozilla/dom/URL.h" +#include "mozilla/ResultExtensions.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MerchantValidationEvent, Event, + mValidationURL, mRequest) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MerchantValidationEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MerchantValidationEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(MerchantValidationEvent, Event) +NS_IMPL_RELEASE_INHERITED(MerchantValidationEvent, Event) + +// User-land code constructor +already_AddRefed<MerchantValidationEvent> MerchantValidationEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MerchantValidationEventInit& aEventInitDict, ErrorResult& aRv) { + // validate passed URL + nsCOMPtr<mozilla::dom::EventTarget> owner = + do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aEventInitDict, aRv); +} + +// Internal JS object constructor +already_AddRefed<MerchantValidationEvent> MerchantValidationEvent::Constructor( + EventTarget* aOwner, const nsAString& aType, + const MerchantValidationEventInit& aEventInitDict, ErrorResult& aRv) { + RefPtr<MerchantValidationEvent> e = new MerchantValidationEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->init(aEventInitDict, aRv); + if (aRv.Failed()) { + return nullptr; + } + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +void MerchantValidationEvent::init( + const MerchantValidationEventInit& aEventInitDict, ErrorResult& aRv) { + // Check methodName is valid + if (!aEventInitDict.mMethodName.IsEmpty()) { + PaymentRequest::IsValidPaymentMethodIdentifier(aEventInitDict.mMethodName, + aRv); + if (aRv.Failed()) { + return; + } + } + SetMethodName(aEventInitDict.mMethodName); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetParentObject()); + auto doc = window->GetExtantDoc(); + if (!doc) { + aRv.ThrowAbortError("The owner document does not exist"); + return; + } + + Result<OwningNonNull<nsIURI>, nsresult> rv = + doc->ResolveWithBaseURI(aEventInitDict.mValidationURL); + if (rv.isErr()) { + aRv.ThrowTypeError("validationURL cannot be parsed"); + return; + } + mValidationURL = rv.unwrap(); +} + +MerchantValidationEvent::MerchantValidationEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr), mWaitForUpdate(false) { + MOZ_ASSERT(aOwner); +} + +void MerchantValidationEvent::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(mRequest); + + if (!mWaitForUpdate) { + return; + } + mWaitForUpdate = false; + + // If we eventually end up supporting merchant validation + // we would validate `aValue` here, as per: + // https://w3c.github.io/payment-request/#validate-merchant-s-details-algorithm + // + // Right now, MerchantValidationEvent is only implemented for standards + // conformance, which is why at this point we throw a + // NS_ERROR_DOM_NOT_SUPPORTED_ERR. + + ErrorResult result; + result.ThrowNotSupportedError( + "complete() is not supported by Firefox currently"); + mRequest->AbortUpdate(result); + mRequest->SetUpdating(false); +} + +void MerchantValidationEvent::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(mRequest); + if (!mWaitForUpdate) { + return; + } + mWaitForUpdate = false; + ErrorResult result; + result.ThrowAbortError( + "The promise for MerchantValidtaionEvent.complete() is rejected"); + mRequest->AbortUpdate(result); + mRequest->SetUpdating(false); +} + +void MerchantValidationEvent::Complete(Promise& aPromise, ErrorResult& aRv) { + if (!IsTrusted()) { + aRv.ThrowInvalidStateError("Called on an untrusted event"); + return; + } + + MOZ_ASSERT(mRequest); + + if (mWaitForUpdate) { + aRv.ThrowInvalidStateError( + "The MerchantValidationEvent is waiting for update"); + return; + } + + if (!mRequest->ReadyForUpdate()) { + aRv.ThrowInvalidStateError( + "The PaymentRequest state is not eInteractive or the PaymentRequest is " + "updating"); + return; + } + + aPromise.AppendNativeHandler(this); + + StopPropagation(); + StopImmediatePropagation(); + mWaitForUpdate = true; + mRequest->SetUpdating(true); +} + +void MerchantValidationEvent::SetRequest(PaymentRequest* aRequest) { + MOZ_ASSERT(IsTrusted()); + MOZ_ASSERT(!mRequest); + MOZ_ASSERT(aRequest); + + mRequest = aRequest; +} + +void MerchantValidationEvent::GetValidationURL(nsAString& aValidationURL) { + nsAutoCString utf8href; + nsresult rv = mValidationURL->GetSpec(utf8href); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + aValidationURL.Assign(NS_ConvertUTF8toUTF16(utf8href)); +} + +void MerchantValidationEvent::GetMethodName(nsAString& aMethodName) { + aMethodName.Assign(mMethodName); +} + +void MerchantValidationEvent::SetMethodName(const nsAString& aMethodName) { + mMethodName.Assign(aMethodName); +} + +MerchantValidationEvent::~MerchantValidationEvent() = default; + +JSObject* MerchantValidationEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MerchantValidationEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/payments/MerchantValidationEvent.h b/dom/payments/MerchantValidationEvent.h new file mode 100644 index 0000000000..e4ace4c6b8 --- /dev/null +++ b/dom/payments/MerchantValidationEvent.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MerchantValidationEvent_h +#define mozilla_dom_MerchantValidationEvent_h + +#include "mozilla/Attributes.h" +#include "mozilla/Result.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MerchantValidationEventBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" + +class nsIURI; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class Promise; +class PaymentRequest; +class MerchantValidationEvent : public Event, public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + MerchantValidationEvent, Event) + + explicit MerchantValidationEvent(EventTarget* aOwner); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + static already_AddRefed<MerchantValidationEvent> Constructor( + EventTarget* aOwner, const nsAString& aType, + const MerchantValidationEventInit& aEventInitDict, ErrorResult& aRv); + + // Called by WebIDL constructor + static already_AddRefed<MerchantValidationEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MerchantValidationEventInit& aEventInitDict, ErrorResult& aRv); + + void Complete(Promise& aPromise, ErrorResult& aRv); + + void SetRequest(PaymentRequest* aRequest); + + void GetValidationURL(nsAString& aValidationURL); + + void GetMethodName(nsAString& aMethodName); + + void SetMethodName(const nsAString& aMethodName); + + protected: + void init(const MerchantValidationEventInit& aEventInitDict, + ErrorResult& aRv); + ~MerchantValidationEvent(); + + private: + // Indicating whether an Complete()-initiated update is currently in progress. + bool mWaitForUpdate; + nsCOMPtr<nsIURI> mValidationURL; + RefPtr<PaymentRequest> mRequest; + nsString mMethodName; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MerchantValidationEvent_h diff --git a/dom/payments/PaymentActionResponse.cpp b/dom/payments/PaymentActionResponse.cpp new file mode 100644 index 0000000000..bca8d7bd76 --- /dev/null +++ b/dom/payments/PaymentActionResponse.cpp @@ -0,0 +1,421 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PaymentActionResponse.h" +#include "BasicCardPayment.h" +#include "PaymentRequestUtils.h" + +namespace mozilla::dom { + +/* PaymentResponseData */ + +NS_IMPL_ISUPPORTS(PaymentResponseData, nsIPaymentResponseData) + +NS_IMETHODIMP +PaymentResponseData::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +PaymentResponseData::Init(const uint32_t aType) { + if (aType != nsIPaymentResponseData::GENERAL_RESPONSE && + aType != nsIPaymentResponseData::BASICCARD_RESPONSE) { + return NS_ERROR_FAILURE; + } + mType = aType; + return NS_OK; +} + +/* GeneralResponseData */ + +NS_IMPL_ISUPPORTS_INHERITED(GeneralResponseData, PaymentResponseData, + nsIGeneralResponseData) + +GeneralResponseData::GeneralResponseData() : mData(u"{}"_ns) { + Init(nsIPaymentResponseData::GENERAL_RESPONSE); +} + +NS_IMETHODIMP +GeneralResponseData::GetData(nsAString& aData) { + aData = mData; + return NS_OK; +} + +NS_IMETHODIMP +GeneralResponseData::InitData(JS::Handle<JS::Value> aValue, JSContext* aCx) { + if (aValue.isNullOrUndefined()) { + return NS_ERROR_FAILURE; + } + nsresult rv = SerializeFromJSVal(aCx, aValue, mData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +/* BasicCardResponseData */ + +NS_IMPL_ISUPPORTS_INHERITED(BasicCardResponseData, PaymentResponseData, + nsIBasicCardResponseData) + +BasicCardResponseData::BasicCardResponseData() { + Init(nsIPaymentResponseData::BASICCARD_RESPONSE); +} + +NS_IMETHODIMP +BasicCardResponseData::GetCardholderName(nsAString& aCardholderName) { + aCardholderName = mCardholderName; + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::GetCardNumber(nsAString& aCardNumber) { + aCardNumber = mCardNumber; + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::GetExpiryMonth(nsAString& aExpiryMonth) { + aExpiryMonth = mExpiryMonth; + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::GetExpiryYear(nsAString& aExpiryYear) { + aExpiryYear = mExpiryYear; + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::GetCardSecurityCode(nsAString& aCardSecurityCode) { + aCardSecurityCode = mCardSecurityCode; + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::GetBillingAddress(nsIPaymentAddress** aBillingAddress) { + NS_ENSURE_ARG_POINTER(aBillingAddress); + nsCOMPtr<nsIPaymentAddress> address; + address = mBillingAddress; + address.forget(aBillingAddress); + return NS_OK; +} + +NS_IMETHODIMP +BasicCardResponseData::InitData(const nsAString& aCardholderName, + const nsAString& aCardNumber, + const nsAString& aExpiryMonth, + const nsAString& aExpiryYear, + const nsAString& aCardSecurityCode, + nsIPaymentAddress* aBillingAddress) { + // cardNumber is a required attribute, cannot be empty; + if (aCardNumber.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + + if (!service->IsValidExpiryMonth(aExpiryMonth)) { + return NS_ERROR_FAILURE; + } + + if (!service->IsValidExpiryYear(aExpiryYear)) { + return NS_ERROR_FAILURE; + } + + mCardholderName = aCardholderName; + mCardNumber = aCardNumber; + mExpiryMonth = aExpiryMonth; + mExpiryYear = aExpiryYear; + mCardSecurityCode = aCardSecurityCode; + mBillingAddress = aBillingAddress; + + return NS_OK; +} + +/* PaymentActionResponse */ + +NS_IMPL_ISUPPORTS(PaymentActionResponse, nsIPaymentActionResponse) + +PaymentActionResponse::PaymentActionResponse() + : mRequestId(u""_ns), mType(nsIPaymentActionResponse::NO_TYPE) {} + +NS_IMETHODIMP +PaymentActionResponse::GetRequestId(nsAString& aRequestId) { + aRequestId = mRequestId; + return NS_OK; +} + +NS_IMETHODIMP +PaymentActionResponse::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + *aType = mType; + return NS_OK; +} + +/* PaymentCanMakeActionResponse */ + +NS_IMPL_ISUPPORTS_INHERITED(PaymentCanMakeActionResponse, PaymentActionResponse, + nsIPaymentCanMakeActionResponse) + +PaymentCanMakeActionResponse::PaymentCanMakeActionResponse() : mResult(false) { + mType = nsIPaymentActionResponse::CANMAKE_ACTION; +} + +NS_IMETHODIMP +PaymentCanMakeActionResponse::GetResult(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mResult; + return NS_OK; +} + +NS_IMETHODIMP +PaymentCanMakeActionResponse::Init(const nsAString& aRequestId, + const bool aResult) { + mRequestId = aRequestId; + mResult = aResult; + return NS_OK; +} + +/* PaymentShowActionResponse */ + +NS_IMPL_ISUPPORTS_INHERITED(PaymentShowActionResponse, PaymentActionResponse, + nsIPaymentShowActionResponse) + +PaymentShowActionResponse::PaymentShowActionResponse() + : mAcceptStatus(nsIPaymentActionResponse::PAYMENT_REJECTED) { + mType = nsIPaymentActionResponse::SHOW_ACTION; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetAcceptStatus(uint32_t* aAcceptStatus) { + NS_ENSURE_ARG_POINTER(aAcceptStatus); + *aAcceptStatus = mAcceptStatus; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetMethodName(nsAString& aMethodName) { + aMethodName = mMethodName; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetData(nsIPaymentResponseData** aData) { + NS_ENSURE_ARG_POINTER(aData); + nsCOMPtr<nsIPaymentResponseData> data = mData; + data.forget(aData); + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetPayerName(nsAString& aPayerName) { + aPayerName = mPayerName; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetPayerEmail(nsAString& aPayerEmail) { + aPayerEmail = mPayerEmail; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::GetPayerPhone(nsAString& aPayerPhone) { + aPayerPhone = mPayerPhone; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShowActionResponse::Init(const nsAString& aRequestId, + const uint32_t aAcceptStatus, + const nsAString& aMethodName, + nsIPaymentResponseData* aData, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone) { + if (aAcceptStatus == nsIPaymentActionResponse::PAYMENT_ACCEPTED) { + NS_ENSURE_ARG_POINTER(aData); + } + + mRequestId = aRequestId; + mAcceptStatus = aAcceptStatus; + mMethodName = aMethodName; + + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + bool isBasicCardPayment = service->IsBasicCardPayment(mMethodName); + + if (aAcceptStatus == nsIPaymentActionResponse::PAYMENT_ACCEPTED) { + uint32_t responseType; + NS_ENSURE_SUCCESS(aData->GetType(&responseType), NS_ERROR_FAILURE); + switch (responseType) { + case nsIPaymentResponseData::GENERAL_RESPONSE: { + if (isBasicCardPayment) { + return NS_ERROR_FAILURE; + } + break; + } + case nsIPaymentResponseData::BASICCARD_RESPONSE: { + if (!isBasicCardPayment) { + return NS_ERROR_FAILURE; + } + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + } + mData = aData; + mPayerName = aPayerName; + mPayerEmail = aPayerEmail; + mPayerPhone = aPayerPhone; + return NS_OK; +} + +/* PaymentAbortActionResponse */ + +NS_IMPL_ISUPPORTS_INHERITED(PaymentAbortActionResponse, PaymentActionResponse, + nsIPaymentAbortActionResponse) + +PaymentAbortActionResponse::PaymentAbortActionResponse() + : mAbortStatus(nsIPaymentActionResponse::ABORT_FAILED) { + mType = nsIPaymentActionResponse::ABORT_ACTION; +} + +NS_IMETHODIMP +PaymentAbortActionResponse::GetAbortStatus(uint32_t* aAbortStatus) { + NS_ENSURE_ARG_POINTER(aAbortStatus); + *aAbortStatus = mAbortStatus; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAbortActionResponse::Init(const nsAString& aRequestId, + const uint32_t aAbortStatus) { + mRequestId = aRequestId; + mAbortStatus = aAbortStatus; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAbortActionResponse::IsSucceeded(bool* aIsSucceeded) { + NS_ENSURE_ARG_POINTER(aIsSucceeded); + *aIsSucceeded = (mAbortStatus == nsIPaymentActionResponse::ABORT_SUCCEEDED); + return NS_OK; +} + +/* PaymentCompleteActionResponse */ + +NS_IMPL_ISUPPORTS_INHERITED(PaymentCompleteActionResponse, + PaymentActionResponse, + nsIPaymentCompleteActionResponse) + +PaymentCompleteActionResponse::PaymentCompleteActionResponse() + : mCompleteStatus(nsIPaymentActionResponse::COMPLETE_FAILED) { + mType = nsIPaymentActionResponse::COMPLETE_ACTION; +} + +nsresult PaymentCompleteActionResponse::Init(const nsAString& aRequestId, + const uint32_t aCompleteStatus) { + mRequestId = aRequestId; + mCompleteStatus = aCompleteStatus; + return NS_OK; +} + +nsresult PaymentCompleteActionResponse::GetCompleteStatus( + uint32_t* aCompleteStatus) { + NS_ENSURE_ARG_POINTER(aCompleteStatus); + *aCompleteStatus = mCompleteStatus; + return NS_OK; +} + +nsresult PaymentCompleteActionResponse::IsCompleted(bool* aIsCompleted) { + NS_ENSURE_ARG_POINTER(aIsCompleted); + *aIsCompleted = + (mCompleteStatus == nsIPaymentActionResponse::COMPLETE_SUCCEEDED); + return NS_OK; +} + +/* PaymentChangeDetails */ + +NS_IMPL_ISUPPORTS(MethodChangeDetails, nsIMethodChangeDetails) + +NS_IMETHODIMP +MethodChangeDetails::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +MethodChangeDetails::Init(const uint32_t aType) { + if (aType != nsIMethodChangeDetails::GENERAL_DETAILS && + aType != nsIMethodChangeDetails::BASICCARD_DETAILS) { + return NS_ERROR_FAILURE; + } + mType = aType; + return NS_OK; +} + +/* GeneralMethodChangeDetails */ + +NS_IMPL_ISUPPORTS_INHERITED(GeneralMethodChangeDetails, MethodChangeDetails, + nsIGeneralChangeDetails) + +GeneralMethodChangeDetails::GeneralMethodChangeDetails() : mDetails(u"{}"_ns) { + Init(nsIMethodChangeDetails::GENERAL_DETAILS); +} + +NS_IMETHODIMP +GeneralMethodChangeDetails::GetDetails(nsAString& aDetails) { + aDetails = mDetails; + return NS_OK; +} + +NS_IMETHODIMP +GeneralMethodChangeDetails::InitData(JS::Handle<JS::Value> aDetails, + JSContext* aCx) { + if (aDetails.isNullOrUndefined()) { + return NS_ERROR_FAILURE; + } + nsresult rv = SerializeFromJSVal(aCx, aDetails, mDetails); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +/* BasicCardMethodChangeDetails */ + +NS_IMPL_ISUPPORTS_INHERITED(BasicCardMethodChangeDetails, MethodChangeDetails, + nsIBasicCardChangeDetails) + +BasicCardMethodChangeDetails::BasicCardMethodChangeDetails() { + Init(nsIMethodChangeDetails::BASICCARD_DETAILS); +} + +NS_IMETHODIMP +BasicCardMethodChangeDetails::GetBillingAddress( + nsIPaymentAddress** aBillingAddress) { + NS_ENSURE_ARG_POINTER(aBillingAddress); + nsCOMPtr<nsIPaymentAddress> address; + address = mBillingAddress; + address.forget(aBillingAddress); + return NS_OK; +} + +NS_IMETHODIMP +BasicCardMethodChangeDetails::InitData(nsIPaymentAddress* aBillingAddress) { + mBillingAddress = aBillingAddress; + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentActionResponse.h b/dom/payments/PaymentActionResponse.h new file mode 100644 index 0000000000..09c0f0d7c1 --- /dev/null +++ b/dom/payments/PaymentActionResponse.h @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentActionResponse_h +#define mozilla_dom_PaymentActionResponse_h + +#include "nsCOMPtr.h" +#include "nsIPaymentActionResponse.h" +#include "nsString.h" + +namespace mozilla::dom { + +class PaymentRequestParent; + +class PaymentResponseData : public nsIPaymentResponseData { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTRESPONSEDATA + + PaymentResponseData() = default; + + protected: + virtual ~PaymentResponseData() = default; + + uint32_t mType; +}; + +class GeneralResponseData final : public PaymentResponseData, + public nsIGeneralResponseData { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTRESPONSEDATA(PaymentResponseData::) + NS_DECL_NSIGENERALRESPONSEDATA + + GeneralResponseData(); + + private: + ~GeneralResponseData() = default; + + nsString mData; +}; + +class BasicCardResponseData final : public nsIBasicCardResponseData, + public PaymentResponseData { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTRESPONSEDATA(PaymentResponseData::) + NS_DECL_NSIBASICCARDRESPONSEDATA + + BasicCardResponseData(); + + private: + ~BasicCardResponseData() = default; + + nsString mCardholderName; + nsString mCardNumber; + nsString mExpiryMonth; + nsString mExpiryYear; + nsString mCardSecurityCode; + nsCOMPtr<nsIPaymentAddress> mBillingAddress; +}; + +class PaymentActionResponse : public nsIPaymentActionResponse { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTACTIONRESPONSE + + PaymentActionResponse(); + + protected: + virtual ~PaymentActionResponse() = default; + + nsString mRequestId; + uint32_t mType; +}; + +class PaymentCanMakeActionResponse final + : public nsIPaymentCanMakeActionResponse, + public PaymentActionResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTACTIONRESPONSE(PaymentActionResponse::) + NS_DECL_NSIPAYMENTCANMAKEACTIONRESPONSE + + PaymentCanMakeActionResponse(); + + private: + ~PaymentCanMakeActionResponse() = default; + + bool mResult; +}; + +class PaymentShowActionResponse final : public nsIPaymentShowActionResponse, + public PaymentActionResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTACTIONRESPONSE(PaymentActionResponse::) + NS_DECL_NSIPAYMENTSHOWACTIONRESPONSE + + PaymentShowActionResponse(); + + private: + ~PaymentShowActionResponse() = default; + + uint32_t mAcceptStatus; + nsString mMethodName; + nsCOMPtr<nsIPaymentResponseData> mData; + nsString mPayerName; + nsString mPayerEmail; + nsString mPayerPhone; +}; + +class PaymentAbortActionResponse final : public nsIPaymentAbortActionResponse, + public PaymentActionResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTACTIONRESPONSE(PaymentActionResponse::) + NS_DECL_NSIPAYMENTABORTACTIONRESPONSE + + PaymentAbortActionResponse(); + + private: + ~PaymentAbortActionResponse() = default; + + uint32_t mAbortStatus; +}; + +class PaymentCompleteActionResponse final + : public nsIPaymentCompleteActionResponse, + public PaymentActionResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIPAYMENTACTIONRESPONSE(PaymentActionResponse::) + NS_DECL_NSIPAYMENTCOMPLETEACTIONRESPONSE + + PaymentCompleteActionResponse(); + + private: + ~PaymentCompleteActionResponse() = default; + + uint32_t mCompleteStatus; +}; + +class MethodChangeDetails : public nsIMethodChangeDetails { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMETHODCHANGEDETAILS + + MethodChangeDetails() = default; + + protected: + virtual ~MethodChangeDetails() = default; + + uint32_t mType; +}; + +class GeneralMethodChangeDetails final : public MethodChangeDetails, + public nsIGeneralChangeDetails { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIMETHODCHANGEDETAILS(MethodChangeDetails::) + NS_DECL_NSIGENERALCHANGEDETAILS + + GeneralMethodChangeDetails(); + + private: + ~GeneralMethodChangeDetails() = default; + + nsString mDetails; +}; + +class BasicCardMethodChangeDetails final : public MethodChangeDetails, + public nsIBasicCardChangeDetails { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIMETHODCHANGEDETAILS(MethodChangeDetails::) + NS_DECL_NSIBASICCARDCHANGEDETAILS + + BasicCardMethodChangeDetails(); + + private: + ~BasicCardMethodChangeDetails() = default; + + nsCOMPtr<nsIPaymentAddress> mBillingAddress; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/PaymentAddress.cpp b/dom/payments/PaymentAddress.cpp new file mode 100644 index 0000000000..86e3b446c7 --- /dev/null +++ b/dom/payments/PaymentAddress.cpp @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PaymentAddress.h" +#include "mozilla/dom/PaymentAddressBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PaymentAddress, mOwner) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PaymentAddress) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PaymentAddress) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentAddress) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +PaymentAddress::PaymentAddress( + nsPIDOMWindowInner* aWindow, const nsAString& aCountry, + const nsTArray<nsString>& aAddressLine, const nsAString& aRegion, + const nsAString& aRegionCode, const nsAString& aCity, + const nsAString& aDependentLocality, const nsAString& aPostalCode, + const nsAString& aSortingCode, const nsAString& aOrganization, + const nsAString& aRecipient, const nsAString& aPhone) + : mCountry(aCountry), + mAddressLine(aAddressLine.Clone()), + mRegion(aRegion), + mRegionCode(aRegionCode), + mCity(aCity), + mDependentLocality(aDependentLocality), + mPostalCode(aPostalCode), + mSortingCode(aSortingCode), + mOrganization(aOrganization), + mRecipient(aRecipient), + mPhone(aPhone), + mOwner(aWindow) {} + +void PaymentAddress::GetCountry(nsAString& aRetVal) const { + aRetVal = mCountry; +} + +void PaymentAddress::GetAddressLine(nsTArray<nsString>& aRetVal) const { + aRetVal = mAddressLine.Clone(); +} + +void PaymentAddress::GetRegion(nsAString& aRetVal) const { aRetVal = mRegion; } + +void PaymentAddress::GetRegionCode(nsAString& aRetVal) const { + aRetVal = mRegionCode; +} + +void PaymentAddress::GetCity(nsAString& aRetVal) const { aRetVal = mCity; } + +void PaymentAddress::GetDependentLocality(nsAString& aRetVal) const { + aRetVal = mDependentLocality; +} + +void PaymentAddress::GetPostalCode(nsAString& aRetVal) const { + aRetVal = mPostalCode; +} + +void PaymentAddress::GetSortingCode(nsAString& aRetVal) const { + aRetVal = mSortingCode; +} + +void PaymentAddress::GetOrganization(nsAString& aRetVal) const { + aRetVal = mOrganization; +} + +void PaymentAddress::GetRecipient(nsAString& aRetVal) const { + aRetVal = mRecipient; +} + +void PaymentAddress::GetPhone(nsAString& aRetVal) const { aRetVal = mPhone; } + +PaymentAddress::~PaymentAddress() = default; + +JSObject* PaymentAddress::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaymentAddress_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentAddress.h b/dom/payments/PaymentAddress.h new file mode 100644 index 0000000000..f57e89f54f --- /dev/null +++ b/dom/payments/PaymentAddress.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentAddress_h +#define mozilla_dom_PaymentAddress_h + +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class PaymentAddress final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PaymentAddress) + + PaymentAddress(nsPIDOMWindowInner* aWindow, const nsAString& aCountry, + const nsTArray<nsString>& aAddressLine, + const nsAString& aRegion, const nsAString& aRegionCode, + const nsAString& aCity, const nsAString& aDependentLocality, + const nsAString& aPostalCode, const nsAString& aSortingCode, + const nsAString& aOrganization, const nsAString& aRecipient, + const nsAString& aPhone); + + nsPIDOMWindowInner* GetParentObject() const { return mOwner; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Getter functions + void GetCountry(nsAString& aRetVal) const; + + void GetAddressLine(nsTArray<nsString>& aRetVal) const; + + void GetRegion(nsAString& aRetVal) const; + + void GetRegionCode(nsAString& aRetVal) const; + + void GetCity(nsAString& aRetVal) const; + + void GetDependentLocality(nsAString& aRetVal) const; + + void GetPostalCode(nsAString& aRetVal) const; + + void GetSortingCode(nsAString& aRetVal) const; + + void GetOrganization(nsAString& aRetVal) const; + + void GetRecipient(nsAString& aRetVal) const; + + void GetPhone(nsAString& aRetVal) const; + + private: + ~PaymentAddress(); + + nsString mCountry; + nsTArray<nsString> mAddressLine; + nsString mRegion; + nsString mRegionCode; + nsString mCity; + nsString mDependentLocality; + nsString mPostalCode; + nsString mSortingCode; + nsString mOrganization; + nsString mRecipient; + nsString mPhone; + + nsCOMPtr<nsPIDOMWindowInner> mOwner; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PaymentAddress_h diff --git a/dom/payments/PaymentMethodChangeEvent.cpp b/dom/payments/PaymentMethodChangeEvent.cpp new file mode 100644 index 0000000000..ba9acf3cf4 --- /dev/null +++ b/dom/payments/PaymentMethodChangeEvent.cpp @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BasicCardPayment.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/PaymentMethodChangeEvent.h" +#include "mozilla/dom/PaymentRequestUpdateEvent.h" +#include "PaymentRequestUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PaymentMethodChangeEvent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PaymentMethodChangeEvent, + PaymentRequestUpdateEvent) + mozilla::DropJSObjects(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PaymentMethodChangeEvent, + PaymentRequestUpdateEvent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentMethodChangeEvent, + PaymentRequestUpdateEvent) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMethodDetails) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(PaymentMethodChangeEvent, PaymentRequestUpdateEvent) +NS_IMPL_RELEASE_INHERITED(PaymentMethodChangeEvent, PaymentRequestUpdateEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentMethodChangeEvent) +NS_INTERFACE_MAP_END_INHERITING(PaymentRequestUpdateEvent) + +already_AddRefed<PaymentMethodChangeEvent> +PaymentMethodChangeEvent::Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict, + const nsAString& aMethodName, const ChangeDetails& aMethodDetails) { + RefPtr<PaymentMethodChangeEvent> e = new PaymentMethodChangeEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + e->SetMethodName(aMethodName); + e->SetMethodDetails(aMethodDetails); + return e.forget(); +} + +already_AddRefed<PaymentMethodChangeEvent> +PaymentMethodChangeEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PaymentMethodChangeEventInit& aEventInitDict) { + nsCOMPtr<mozilla::dom::EventTarget> owner = + do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<PaymentMethodChangeEvent> e = new PaymentMethodChangeEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + e->init(aEventInitDict); + return e.forget(); +} + +PaymentMethodChangeEvent::PaymentMethodChangeEvent(EventTarget* aOwner) + : PaymentRequestUpdateEvent(aOwner) { + MOZ_ASSERT(aOwner); + mozilla::HoldJSObjects(this); +} + +void PaymentMethodChangeEvent::init( + const PaymentMethodChangeEventInit& aEventInitDict) { + mMethodName.Assign(aEventInitDict.mMethodName); + mMethodDetails = aEventInitDict.mMethodDetails; +} + +void PaymentMethodChangeEvent::GetMethodName(nsAString& aMethodName) { + aMethodName.Assign(mMethodName); +} + +void PaymentMethodChangeEvent::SetMethodName(const nsAString& aMethodName) { + mMethodName = aMethodName; +} + +void PaymentMethodChangeEvent::GetMethodDetails( + JSContext* aCx, JS::MutableHandle<JSObject*> aRetVal) { + MOZ_ASSERT(aCx); + + if (mMethodDetails) { + aRetVal.set(mMethodDetails.get()); + return; + } + + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + aRetVal.set(nullptr); + switch (mInternalDetails.type()) { + case ChangeDetails::GeneralMethodDetails: { + const GeneralDetails& rawDetails = mInternalDetails.generalDetails(); + DeserializeToJSObject(rawDetails.details, aCx, aRetVal); + break; + } + case ChangeDetails::BasicCardMethodDetails: { + const BasicCardDetails& rawDetails = mInternalDetails.basicCardDetails(); + BasicCardChangeDetails basicCardDetails; + PaymentOptions options; + mRequest->GetOptions(options); + if (options.mRequestBillingAddress) { + if (!rawDetails.billingAddress.country.IsEmpty() || + !rawDetails.billingAddress.addressLine.IsEmpty() || + !rawDetails.billingAddress.region.IsEmpty() || + !rawDetails.billingAddress.regionCode.IsEmpty() || + !rawDetails.billingAddress.city.IsEmpty() || + !rawDetails.billingAddress.dependentLocality.IsEmpty() || + !rawDetails.billingAddress.postalCode.IsEmpty() || + !rawDetails.billingAddress.sortingCode.IsEmpty() || + !rawDetails.billingAddress.organization.IsEmpty() || + !rawDetails.billingAddress.recipient.IsEmpty() || + !rawDetails.billingAddress.phone.IsEmpty()) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(GetParentObject()); + basicCardDetails.mBillingAddress = + new PaymentAddress(window, rawDetails.billingAddress.country, + rawDetails.billingAddress.addressLine, + rawDetails.billingAddress.region, + rawDetails.billingAddress.regionCode, + rawDetails.billingAddress.city, + rawDetails.billingAddress.dependentLocality, + rawDetails.billingAddress.postalCode, + rawDetails.billingAddress.sortingCode, + rawDetails.billingAddress.organization, + rawDetails.billingAddress.recipient, + rawDetails.billingAddress.phone); + } + } + MOZ_ASSERT(aCx); + JS::Rooted<JS::Value> value(aCx); + if (NS_WARN_IF(!basicCardDetails.ToObjectInternal(aCx, &value))) { + return; + } + aRetVal.set(&value.toObject()); + break; + } + default: { + break; + } + } +} + +void PaymentMethodChangeEvent::SetMethodDetails( + const ChangeDetails& aMethodDetails) { + mInternalDetails = aMethodDetails; +} + +PaymentMethodChangeEvent::~PaymentMethodChangeEvent() { + mozilla::DropJSObjects(this); +} + +JSObject* PaymentMethodChangeEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return PaymentMethodChangeEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentMethodChangeEvent.h b/dom/payments/PaymentMethodChangeEvent.h new file mode 100644 index 0000000000..558c19530a --- /dev/null +++ b/dom/payments/PaymentMethodChangeEvent.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentMethodChangeEvent_h +#define mozilla_dom_PaymentMethodChangeEvent_h + +#include "mozilla/dom/PaymentMethodChangeEventBinding.h" +#include "mozilla/dom/PaymentRequestUpdateEvent.h" +#include "mozilla/dom/PaymentRequest.h" + +namespace mozilla::dom { +class PaymentRequestUpdateEvent; +struct PaymentMethodChangeEventInit; +class PaymentMethodChangeEvent final : public PaymentRequestUpdateEvent { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + PaymentMethodChangeEvent, PaymentRequestUpdateEvent) + + explicit PaymentMethodChangeEvent(EventTarget* aOwner); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<PaymentMethodChangeEvent> Constructor( + EventTarget* aOwner, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict, + const nsAString& aMethodName, const ChangeDetails& aMethodDetails); + + // Called by WebIDL constructor + static already_AddRefed<PaymentMethodChangeEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PaymentMethodChangeEventInit& aEventInitDict); + + void GetMethodName(nsAString& aMethodName); + void SetMethodName(const nsAString& aMethodName); + + void GetMethodDetails(JSContext* cx, JS::MutableHandle<JSObject*> retval); + void SetMethodDetails(const ChangeDetails& aMethodDetails); + + protected: + void init(const PaymentMethodChangeEventInit& aEventInitDict); + ~PaymentMethodChangeEvent(); + + private: + JS::Heap<JSObject*> mMethodDetails; + ChangeDetails mInternalDetails; + nsString mMethodName; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PaymentMethodChangeEvent_h diff --git a/dom/payments/PaymentRequest.cpp b/dom/payments/PaymentRequest.cpp new file mode 100644 index 0000000000..00f6345f0b --- /dev/null +++ b/dom/payments/PaymentRequest.cpp @@ -0,0 +1,1261 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BasicCardPayment.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/PaymentMethodChangeEvent.h" +#include "mozilla/dom/PaymentRequest.h" +#include "mozilla/dom/PaymentRequestChild.h" +#include "mozilla/dom/PaymentRequestManager.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsContentUtils.h" +#include "nsIDUtils.h" +#include "nsImportModule.h" +#include "nsIRegion.h" +#include "nsIScriptError.h" +#include "nsIURLParser.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/MerchantValidationEvent.h" +#include "PaymentResponse.h" + +using mozilla::intl::LocaleService; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PaymentRequest) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResultPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAcceptPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbortPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponse) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFullShippingAddress) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PaymentRequest, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResultPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAcceptPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAbortPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResponse) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFullShippingAddress) + tmp->UnregisterActivityObserver(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentRequest) + NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(PaymentRequest, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(PaymentRequest, DOMEventTargetHelper) + +bool PaymentRequest::PrefEnabled(JSContext* aCx, JSObject* aObj) { +#if defined(NIGHTLY_BUILD) + if (!XRE_IsContentProcess()) { + return false; + } + if (!StaticPrefs::dom_payments_request_enabled()) { + return false; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + + nsCOMPtr<nsIRegion> regionJsm = + do_ImportESModule("resource://gre/modules/Region.sys.mjs", "Region"); + nsAutoString region; + nsresult rv = regionJsm->GetHome(region); + if (NS_FAILED(rv)) { + return false; + } + + if (!manager->IsRegionSupported(region)) { + return false; + } + nsAutoCString locale; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale); + mozilla::intl::Locale loc; + auto result = mozilla::intl::LocaleParser::TryParse(locale, loc); + if (!(result.isOk() && loc.Canonicalize().isOk() && + loc.Language().EqualTo("en") && loc.Region().EqualTo("US"))) { + return false; + } + + return true; +#else + return false; +#endif +} + +void PaymentRequest::IsValidStandardizedPMI(const nsAString& aIdentifier, + ErrorResult& aRv) { + /* + * The syntax of a standardized payment method identifier is given by the + * following [ABNF]: + * + * stdpmi = part *( "-" part ) + * part = 1loweralpha *( DIGIT / loweralpha ) + * loweralpha = %x61-7A + */ + const char16_t* start = aIdentifier.BeginReading(); + const char16_t* end = aIdentifier.EndReading(); + while (start != end) { + // the first char must be in the range %x61-7A + if ((*start < 'a' || *start > 'z')) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral( + "' at the beginning or after the '-' must be in the range [a-z]."); + aRv.ThrowRangeError(error); + return; + } + ++start; + // the rest can be in the range %x61-7A + DIGITs + while (start != end && *start != '-' && + ((*start >= 'a' && *start <= 'z') || + (*start >= '0' && *start <= '9'))) { + ++start; + } + // if the char is not in the range %x61-7A + DIGITs, it must be '-' + if (start != end && *start != '-') { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral("' must be in the range [a-zA-z0-9-]."); + aRv.ThrowRangeError(error); + return; + } + if (*start == '-') { + ++start; + // the last char can not be '-' + if (start == end) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The last character '"); + error.Append(NS_ConvertUTF16toUTF8(start, 1)); + error.AppendLiteral("' must be in the range [a-z0-9]."); + aRv.ThrowRangeError(error); + return; + } + } + } +} + +void PaymentRequest::IsValidPaymentMethodIdentifier( + const nsAString& aIdentifier, ErrorResult& aRv) { + if (aIdentifier.IsEmpty()) { + aRv.ThrowTypeError("Payment method identifier is required."); + return; + } + /* + * URL-based payment method identifier + * + * 1. If url's scheme is not "https", return false. + * 2. If url's username or password is not the empty string, return false. + * 3. Otherwise, return true. + */ + nsCOMPtr<nsIURLParser> urlParser = do_GetService(NS_STDURLPARSER_CONTRACTID); + MOZ_ASSERT(urlParser); + uint32_t schemePos = 0; + int32_t schemeLen = 0; + uint32_t authorityPos = 0; + int32_t authorityLen = 0; + NS_ConvertUTF16toUTF8 url(aIdentifier); + nsresult rv = + urlParser->ParseURL(url.get(), url.Length(), &schemePos, &schemeLen, + &authorityPos, &authorityLen, nullptr, nullptr); + if (NS_FAILED(rv)) { + nsAutoCString error; + error.AppendLiteral("Error parsing payment method identifier '"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("'as a URL."); + aRv.ThrowRangeError(error); + return; + } + + if (schemeLen == -1) { + // The PMI is not a URL-based PMI, check if it is a standardized PMI + IsValidStandardizedPMI(aIdentifier, aRv); + return; + } + if (!Substring(aIdentifier, schemePos, schemeLen).EqualsASCII("https")) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. The scheme must be 'https'."); + aRv.ThrowRangeError(error); + return; + } + if (Substring(aIdentifier, authorityPos, authorityLen).IsEmpty()) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } + + uint32_t usernamePos = 0; + int32_t usernameLen = 0; + uint32_t passwordPos = 0; + int32_t passwordLen = 0; + uint32_t hostnamePos = 0; + int32_t hostnameLen = 0; + int32_t port = 0; + + NS_ConvertUTF16toUTF8 authority( + Substring(aIdentifier, authorityPos, authorityLen)); + rv = urlParser->ParseAuthority( + authority.get(), authority.Length(), &usernamePos, &usernameLen, + &passwordPos, &passwordLen, &hostnamePos, &hostnameLen, &port); + if (NS_FAILED(rv)) { + // Handle the special cases that URLParser treats it as an invalid URL, but + // are used in web-platform-test + // For exmaple: + // https://:@example.com // should be considered as valid + // https://:password@example.com. // should be considered as invalid + int32_t atPos = authority.FindChar('@'); + if (atPos >= 0) { + // only accept the case https://:@xxx + if (atPos == 1 && authority.CharAt(0) == ':') { + usernamePos = 0; + usernameLen = 0; + passwordPos = 0; + passwordLen = 0; + } else { + // for the fail cases, don't care about what the actual length is. + usernamePos = 0; + usernameLen = INT32_MAX; + passwordPos = 0; + passwordLen = INT32_MAX; + } + } else { + usernamePos = 0; + usernameLen = -1; + passwordPos = 0; + passwordLen = -1; + } + // Parse server information when both username and password are empty or do + // not exist. + if ((usernameLen <= 0) && (passwordLen <= 0)) { + if (authority.Length() - atPos - 1 == 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } + // Re-using nsIURLParser::ParseServerInfo to extract the hostname and port + // information. This can help us to handle complicated IPv6 cases. + nsAutoCString serverInfo( + Substring(authority, atPos + 1, authority.Length() - atPos - 1)); + rv = urlParser->ParseServerInfo(serverInfo.get(), serverInfo.Length(), + &hostnamePos, &hostnameLen, &port); + if (NS_FAILED(rv)) { + // ParseServerInfo returns NS_ERROR_MALFORMED_URI in all fail cases, we + // probably need a followup bug to figure out the fail reason. + nsAutoCString error; + error.AssignLiteral("Error extracting hostname from '"); + error.Append(serverInfo); + error.AppendLiteral("'."); + aRv.ThrowRangeError(error); + return; + } + } + } + // PMI is valid when usernameLen/passwordLen equals to -1 or 0. + if (usernameLen > 0 || passwordLen > 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AssignLiteral("' is not valid. Username and password must be empty."); + aRv.ThrowRangeError(error); + return; + } + + // PMI is valid when hostnameLen is larger than 0 + if (hostnameLen <= 0) { + nsAutoCString error; + error.AssignLiteral("'"); + error.Append(NS_ConvertUTF16toUTF8(aIdentifier)); + error.AppendLiteral("' is not valid. hostname can not be empty."); + aRv.ThrowRangeError(error); + return; + } +} + +void PaymentRequest::IsValidMethodData( + JSContext* aCx, const Sequence<PaymentMethodData>& aMethodData, + ErrorResult& aRv) { + if (!aMethodData.Length()) { + aRv.ThrowTypeError("At least one payment method is required."); + return; + } + + nsTArray<nsString> methods; + for (const PaymentMethodData& methodData : aMethodData) { + IsValidPaymentMethodIdentifier(methodData.mSupportedMethods, aRv); + if (aRv.Failed()) { + return; + } + + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + if (service->IsBasicCardPayment(methodData.mSupportedMethods)) { + if (!methodData.mData.WasPassed()) { + continue; + } + MOZ_ASSERT(aCx); + nsAutoString error; + if (!service->IsValidBasicCardRequest(aCx, methodData.mData.Value(), + error)) { + aRv.ThrowTypeError(NS_ConvertUTF16toUTF8(error)); + return; + } + } + if (!methods.Contains(methodData.mSupportedMethods)) { + methods.AppendElement(methodData.mSupportedMethods); + } else { + aRv.ThrowRangeError(nsPrintfCString( + "Duplicate payment method '%s'", + NS_ConvertUTF16toUTF8(methodData.mSupportedMethods).get())); + return; + } + } +} + +void PaymentRequest::IsValidNumber(const nsAString& aItem, + const nsAString& aStr, ErrorResult& aRv) { + nsresult error = NS_ERROR_FAILURE; + + if (!aStr.IsEmpty()) { + nsAutoString aValue(aStr); + + // If the beginning character is '-', we will check the second one. + int beginningIndex = (aValue.First() == '-') ? 1 : 0; + + // Ensure + // - the beginning character is a digit in [0-9], and + // - the last character is not '.' + // to follow spec: + // https://w3c.github.io/browser-payment-api/#dfn-valid-decimal-monetary-value + // + // For example, ".1" is not valid for '.' is not in [0-9], + // and " 0.1" either for beginning with ' ' + if (aValue.Last() != '.' && aValue.CharAt(beginningIndex) >= '0' && + aValue.CharAt(beginningIndex) <= '9') { + aValue.ToFloat(&error); + } + } + + if (NS_FAILED(error)) { + nsAutoCString errorMsg; + errorMsg.AssignLiteral("The amount.value of \""); + errorMsg.Append(NS_ConvertUTF16toUTF8(aItem)); + errorMsg.AppendLiteral("\"("); + errorMsg.Append(NS_ConvertUTF16toUTF8(aStr)); + errorMsg.AppendLiteral(") must be a valid decimal monetary value."); + aRv.ThrowTypeError(errorMsg); + return; + } +} + +void PaymentRequest::IsNonNegativeNumber(const nsAString& aItem, + const nsAString& aStr, + ErrorResult& aRv) { + nsresult error = NS_ERROR_FAILURE; + + if (!aStr.IsEmpty()) { + nsAutoString aValue(aStr); + // Ensure + // - the beginning character is a digit in [0-9], and + // - the last character is not '.' + if (aValue.Last() != '.' && aValue.First() >= '0' && + aValue.First() <= '9') { + aValue.ToFloat(&error); + } + } + + if (NS_FAILED(error)) { + nsAutoCString errorMsg; + errorMsg.AssignLiteral("The amount.value of \""); + errorMsg.Append(NS_ConvertUTF16toUTF8(aItem)); + errorMsg.AppendLiteral("\"("); + errorMsg.Append(NS_ConvertUTF16toUTF8(aStr)); + errorMsg.AppendLiteral( + ") must be a valid and non-negative decimal monetary value."); + aRv.ThrowTypeError(errorMsg); + return; + } +} + +void PaymentRequest::IsValidCurrency(const nsAString& aItem, + const nsAString& aCurrency, + ErrorResult& aRv) { + /* + * According to spec in + * https://w3c.github.io/payment-request/#validity-checkers, perform currency + * validation with following criteria + * 1. The currency length must be 3. + * 2. The currency contains any character that must be in the range "A" to + * "Z" (U+0041 to U+005A) or the range "a" to "z" (U+0061 to U+007A) + */ + if (aCurrency.Length() != 3) { + nsAutoCString error; + error.AssignLiteral("The length amount.currency of \""); + error.Append(NS_ConvertUTF16toUTF8(aItem)); + error.AppendLiteral("\"("); + error.Append(NS_ConvertUTF16toUTF8(aCurrency)); + error.AppendLiteral(") must be 3."); + aRv.ThrowRangeError(error); + return; + } + // Don't use nsUnicharUtils::ToUpperCase, it converts the invalid "ınr" PMI to + // to the valid one "INR". + for (uint32_t idx = 0; idx < aCurrency.Length(); ++idx) { + if ((aCurrency.CharAt(idx) >= 'A' && aCurrency.CharAt(idx) <= 'Z') || + (aCurrency.CharAt(idx) >= 'a' && aCurrency.CharAt(idx) <= 'z')) { + continue; + } + nsAutoCString error; + error.AssignLiteral("The character amount.currency of \""); + error.Append(NS_ConvertUTF16toUTF8(aItem)); + error.AppendLiteral("\"("); + error.Append(NS_ConvertUTF16toUTF8(aCurrency)); + error.AppendLiteral( + ") must be in the range 'A' to 'Z'(U+0041 to U+005A) or 'a' to " + "'z'(U+0061 to U+007A)."); + aRv.ThrowRangeError(error); + return; + } +} + +void PaymentRequest::IsValidCurrencyAmount(const nsAString& aItem, + const PaymentCurrencyAmount& aAmount, + const bool aIsTotalItem, + ErrorResult& aRv) { + IsValidCurrency(aItem, aAmount.mCurrency, aRv); + if (aRv.Failed()) { + return; + } + if (aIsTotalItem) { + IsNonNegativeNumber(aItem, aAmount.mValue, aRv); + if (aRv.Failed()) { + return; + } + } else { + IsValidNumber(aItem, aAmount.mValue, aRv); + if (aRv.Failed()) { + return; + } + } +} + +void PaymentRequest::IsValidDetailsInit(const PaymentDetailsInit& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value and amount.currency of detail.total + IsValidCurrencyAmount(u"details.total"_ns, aDetails.mTotal.mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + return IsValidDetailsBase(aDetails, aRequestShipping, aRv); +} + +void PaymentRequest::IsValidDetailsUpdate(const PaymentDetailsUpdate& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value and amount.currency of detail.total + if (aDetails.mTotal.WasPassed()) { + IsValidCurrencyAmount(u"details.total"_ns, aDetails.mTotal.Value().mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + IsValidDetailsBase(aDetails, aRequestShipping, aRv); +} + +void PaymentRequest::IsValidDetailsBase(const PaymentDetailsBase& aDetails, + const bool aRequestShipping, + ErrorResult& aRv) { + // Check the amount.value of each item in the display items + if (aDetails.mDisplayItems.WasPassed()) { + const Sequence<PaymentItem>& displayItems = aDetails.mDisplayItems.Value(); + for (const PaymentItem& displayItem : displayItems) { + IsValidCurrencyAmount(displayItem.mLabel, displayItem.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + } + + // Check the shipping option + if (aDetails.mShippingOptions.WasPassed() && aRequestShipping) { + const Sequence<PaymentShippingOption>& shippingOptions = + aDetails.mShippingOptions.Value(); + nsTArray<nsString> seenIDs; + for (const PaymentShippingOption& shippingOption : shippingOptions) { + IsValidCurrencyAmount(u"details.shippingOptions"_ns, + shippingOption.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + if (seenIDs.Contains(shippingOption.mId)) { + nsAutoCString error; + error.AssignLiteral("Duplicate shippingOption id '"); + error.Append(NS_ConvertUTF16toUTF8(shippingOption.mId)); + error.AppendLiteral("'"); + aRv.ThrowTypeError(error); + return; + } + seenIDs.AppendElement(shippingOption.mId); + } + } + + // Check payment details modifiers + if (aDetails.mModifiers.WasPassed()) { + const Sequence<PaymentDetailsModifier>& modifiers = + aDetails.mModifiers.Value(); + for (const PaymentDetailsModifier& modifier : modifiers) { + IsValidPaymentMethodIdentifier(modifier.mSupportedMethods, aRv); + if (aRv.Failed()) { + return; + } + if (modifier.mTotal.WasPassed()) { + IsValidCurrencyAmount(u"details.modifiers.total"_ns, + modifier.mTotal.Value().mAmount, + true, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + if (modifier.mAdditionalDisplayItems.WasPassed()) { + const Sequence<PaymentItem>& displayItems = + modifier.mAdditionalDisplayItems.Value(); + for (const PaymentItem& displayItem : displayItems) { + IsValidCurrencyAmount(displayItem.mLabel, displayItem.mAmount, + false, // isTotalItem + aRv); + if (aRv.Failed()) { + return; + } + } + } + } + } +} + +already_AddRefed<PaymentRequest> PaymentRequest::Constructor( + const GlobalObject& aGlobal, const Sequence<PaymentMethodData>& aMethodData, + const PaymentDetailsInit& aDetails, const PaymentOptions& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.ThrowAbortError("No global object for creating PaymentRequest"); + return nullptr; + } + + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (!doc) { + aRv.ThrowAbortError("No document for creating PaymentRequest"); + return nullptr; + } + + // the feature can only be used in an active document + if (!doc->IsCurrentActiveDocument()) { + aRv.ThrowSecurityError( + "Can't create a PaymentRequest for an inactive document"); + return nullptr; + } + + if (!FeaturePolicyUtils::IsFeatureAllowed(doc, u"payment"_ns)) { + aRv.ThrowSecurityError( + "Document's Feature Policy does not allow to create a PaymentRequest"); + return nullptr; + } + + // Get the top same process document + nsCOMPtr<Document> topSameProcessDoc = doc; + topSameProcessDoc = doc; + while (topSameProcessDoc) { + nsCOMPtr<Document> parent = topSameProcessDoc->GetInProcessParentDocument(); + if (!parent || !parent->IsContentDocument()) { + break; + } + topSameProcessDoc = parent; + } + nsCOMPtr<nsIPrincipal> topLevelPrincipal = topSameProcessDoc->NodePrincipal(); + + // Check payment methods and details + IsValidMethodData(aGlobal.Context(), aMethodData, aRv); + if (aRv.Failed()) { + return nullptr; + } + IsValidDetailsInit(aDetails, aOptions.mRequestShipping, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + if (NS_WARN_IF(!manager)) { + return nullptr; + } + + // Create PaymentRequest and set its |mId| + RefPtr<PaymentRequest> request; + manager->CreatePayment(aGlobal.Context(), window, topLevelPrincipal, + aMethodData, aDetails, aOptions, + getter_AddRefs(request), aRv); + if (aRv.Failed()) { + return nullptr; + } + return request.forget(); +} + +already_AddRefed<PaymentRequest> PaymentRequest::CreatePaymentRequest( + nsPIDOMWindowInner* aWindow, ErrorResult& aRv) { + // Generate a unique id for identification + nsID uuid; + if (NS_WARN_IF(NS_FAILED(nsID::GenerateUUIDInPlace(uuid)))) { + aRv.ThrowAbortError( + "Failed to create an internal UUID for the PaymentRequest"); + return nullptr; + } + + NSID_TrimBracketsUTF16 id(uuid); + + // Create payment request with generated id + RefPtr<PaymentRequest> request = new PaymentRequest(aWindow, id); + return request.forget(); +} + +PaymentRequest::PaymentRequest(nsPIDOMWindowInner* aWindow, + const nsAString& aInternalId) + : DOMEventTargetHelper(aWindow), + mInternalId(aInternalId), + mShippingAddress(nullptr), + mUpdating(false), + mRequestShipping(false), + mState(eCreated), + mIPC(nullptr) { + MOZ_ASSERT(aWindow); + RegisterActivityObserver(); +} + +already_AddRefed<Promise> PaymentRequest::CanMakePayment(ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + if (mState != eCreated) { + aRv.ThrowInvalidStateError( + "The PaymentRequest's state should be 'Created'"); + return nullptr; + } + + if (mResultPromise) { + // XXX This doesn't match the spec but does match Chromium. + aRv.ThrowNotAllowedError( + "PaymentRequest.CanMakePayment() has already been called"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->CanMakePayment(this, aRv); + if (aRv.Failed()) { + return nullptr; + } + mResultPromise = promise; + return promise.forget(); +} + +void PaymentRequest::RespondCanMakePayment(bool aResult) { + MOZ_ASSERT(mResultPromise); + mResultPromise->MaybeResolve(aResult); + mResultPromise = nullptr; +} + +already_AddRefed<Promise> PaymentRequest::Show( + const Optional<OwningNonNull<Promise>>& aDetailsPromise, ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global); + Document* doc = win->GetExtantDoc(); + + if (!UserActivation::IsHandlingUserInput()) { + nsString msg = nsLiteralString( + u"User activation is now required to call PaymentRequest.show()"); + nsContentUtils::ReportToConsoleNonLocalized( + msg, nsIScriptError::warningFlag, "Security"_ns, doc); + if (StaticPrefs::dom_payments_request_user_interaction_required()) { + aRv.ThrowSecurityError(NS_ConvertUTF16toUTF8(msg)); + return nullptr; + } + } + + if (mState != eCreated) { + aRv.ThrowInvalidStateError( + "The PaymentRequest's state should be 'Created'"); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + mState = eClosed; + return nullptr; + } + + if (aDetailsPromise.WasPassed()) { + aDetailsPromise.Value().AppendNativeHandler(this); + mUpdating = true; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->ShowPayment(this, aRv); + if (aRv.Failed()) { + mState = eClosed; + return nullptr; + } + + mAcceptPromise = promise; + mState = eInteractive; + return promise.forget(); +} + +void PaymentRequest::RejectShowPayment(ErrorResult&& aRejectReason) { + MOZ_ASSERT(mAcceptPromise || mResponse); + MOZ_ASSERT(mState == eInteractive); + + if (mResponse) { + mResponse->RejectRetry(std::move(aRejectReason)); + } else { + mAcceptPromise->MaybeReject(std::move(aRejectReason)); + } + mState = eClosed; + mAcceptPromise = nullptr; +} + +void PaymentRequest::RespondShowPayment(const nsAString& aMethodName, + const ResponseData& aDetails, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone, + ErrorResult&& aResult) { + MOZ_ASSERT(mState == eInteractive); + + if (aResult.Failed()) { + RejectShowPayment(std::move(aResult)); + return; + } + + // https://github.com/w3c/payment-request/issues/692 + mShippingAddress.swap(mFullShippingAddress); + mFullShippingAddress = nullptr; + + if (mResponse) { + mResponse->RespondRetry(aMethodName, mShippingOption, mShippingAddress, + aDetails, aPayerName, aPayerEmail, aPayerPhone); + } else if (mAcceptPromise) { + RefPtr<PaymentResponse> paymentResponse = new PaymentResponse( + GetOwner(), this, mId, aMethodName, mShippingOption, mShippingAddress, + aDetails, aPayerName, aPayerEmail, aPayerPhone); + mResponse = paymentResponse; + mAcceptPromise->MaybeResolve(paymentResponse); + } else { + // mAccpetPromise could be nulled through document activity changed. And + // there is nothing to do here. + mState = eClosed; + return; + } + + mState = eClosed; + mAcceptPromise = nullptr; +} + +void PaymentRequest::RespondComplete() { + MOZ_ASSERT(mResponse); + mResponse->RespondComplete(); +} + +already_AddRefed<Promise> PaymentRequest::Abort(ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + if (mState != eInteractive) { + aRv.ThrowSecurityError( + "The PaymentRequest's state should be 'Interactive'"); + return nullptr; + } + + if (mAbortPromise) { + aRv.ThrowInvalidStateError( + "PaymentRequest.abort() has already been called"); + return nullptr; + } + + nsIGlobalObject* global = GetOwnerGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->AbortPayment(this, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mAbortPromise = promise; + return promise.forget(); +} + +void PaymentRequest::RespondAbortPayment(bool aSuccess) { + // Check whether we are aborting the update: + // + // - If |mUpdateError| is failed, we are aborting the update as + // |mUpdateError| was set in method |AbortUpdate|. + // => Reject |mAcceptPromise| and reset |mUpdateError| to complete + // the action, regardless of |aSuccess|. + // + // - Otherwise, we are handling |Abort| method call from merchant. + // => Resolve/Reject |mAbortPromise| based on |aSuccess|. + if (mUpdateError.Failed()) { + // Respond show with mUpdateError, set mUpdating to false. + mUpdating = false; + RespondShowPayment(u""_ns, ResponseData(), u""_ns, u""_ns, u""_ns, + std::move(mUpdateError)); + return; + } + + if (mState != eInteractive) { + return; + } + + if (mAbortPromise) { + if (aSuccess) { + mAbortPromise->MaybeResolve(JS::UndefinedHandleValue); + mAbortPromise = nullptr; + ErrorResult abortResult; + abortResult.ThrowAbortError("The PaymentRequest is aborted"); + RejectShowPayment(std::move(abortResult)); + } else { + mAbortPromise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + mAbortPromise = nullptr; + } + } +} + +void PaymentRequest::UpdatePayment(JSContext* aCx, + const PaymentDetailsUpdate& aDetails, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + if (mState != eInteractive) { + aRv.ThrowInvalidStateError( + "The PaymentRequest state should be 'Interactive'"); + return; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->UpdatePayment(aCx, this, aDetails, mRequestShipping, aRv); +} + +void PaymentRequest::AbortUpdate(ErrorResult& aReason) { + // AbortUpdate has the responsiblity to call aReason.SuppressException() when + // fail to update. + + MOZ_ASSERT(aReason.Failed()); + + // Completely ignoring the call when the owner document is not fully active. + if (!InFullyActiveDocument()) { + aReason.SuppressException(); + return; + } + + // Completely ignoring the call when the PaymentRequest state is not + // eInteractive. + if (mState != eInteractive) { + aReason.SuppressException(); + return; + } + // Try to close down any remaining user interface. Should recevie + // RespondAbortPayment from chrome process. + // Completely ignoring the call when failed to send action to chrome process. + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + IgnoredErrorResult result; + manager->AbortPayment(this, result); + if (result.Failed()) { + aReason.SuppressException(); + return; + } + + // Remember update error |aReason| and do the following steps in + // RespondShowPayment. + // 1. Set target.state to closed + // 2. Reject the promise target.acceptPromise with exception "aRv" + // 3. Abort the algorithm with update error + mUpdateError = std::move(aReason); +} + +void PaymentRequest::RetryPayment(JSContext* aCx, + const PaymentValidationErrors& aErrors, + ErrorResult& aRv) { + if (mState == eInteractive) { + aRv.ThrowInvalidStateError( + "Call Retry() when the PaymentReqeust state is 'Interactive'"); + return; + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->RetryPayment(aCx, this, aErrors, aRv); + if (aRv.Failed()) { + return; + } + mState = eInteractive; +} + +void PaymentRequest::GetId(nsAString& aRetVal) const { aRetVal = mId; } + +void PaymentRequest::GetInternalId(nsAString& aRetVal) { + aRetVal = mInternalId; +} + +void PaymentRequest::SetId(const nsAString& aId) { mId = aId; } + +bool PaymentRequest::Equals(const nsAString& aInternalId) const { + return mInternalId.Equals(aInternalId); +} + +bool PaymentRequest::ReadyForUpdate() { + return mState == eInteractive && !mUpdating; +} + +void PaymentRequest::SetUpdating(bool aUpdating) { mUpdating = aUpdating; } + +already_AddRefed<PaymentResponse> PaymentRequest::GetResponse() const { + RefPtr<PaymentResponse> response = mResponse; + return response.forget(); +} + +nsresult PaymentRequest::DispatchUpdateEvent(const nsAString& aType) { + MOZ_ASSERT(ReadyForUpdate()); + + PaymentRequestUpdateEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<PaymentRequestUpdateEvent> event = + PaymentRequestUpdateEvent::Constructor(this, aType, init); + event->SetTrusted(true); + event->SetRequest(this); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +nsresult PaymentRequest::DispatchMerchantValidationEvent( + const nsAString& aType) { + MOZ_ASSERT(ReadyForUpdate()); + + MerchantValidationEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mValidationURL.Truncate(); + + ErrorResult rv; + RefPtr<MerchantValidationEvent> event = + MerchantValidationEvent::Constructor(this, aType, init, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + event->SetTrusted(true); + event->SetRequest(this); + + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +nsresult PaymentRequest::DispatchPaymentMethodChangeEvent( + const nsAString& aMethodName, const ChangeDetails& aMethodDetails) { + MOZ_ASSERT(ReadyForUpdate()); + + PaymentRequestUpdateEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<PaymentMethodChangeEvent> event = + PaymentMethodChangeEvent::Constructor(this, u"paymentmethodchange"_ns, + init, aMethodName, aMethodDetails); + event->SetTrusted(true); + event->SetRequest(this); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +already_AddRefed<PaymentAddress> PaymentRequest::GetShippingAddress() const { + RefPtr<PaymentAddress> address = mShippingAddress; + return address.forget(); +} + +nsresult PaymentRequest::UpdateShippingAddress( + const nsAString& aCountry, const nsTArray<nsString>& aAddressLine, + const nsAString& aRegion, const nsAString& aRegionCode, + const nsAString& aCity, const nsAString& aDependentLocality, + const nsAString& aPostalCode, const nsAString& aSortingCode, + const nsAString& aOrganization, const nsAString& aRecipient, + const nsAString& aPhone) { + nsTArray<nsString> emptyArray; + mShippingAddress = new PaymentAddress( + GetOwner(), aCountry, emptyArray, aRegion, aRegionCode, aCity, + aDependentLocality, aPostalCode, aSortingCode, u""_ns, u""_ns, u""_ns); + mFullShippingAddress = + new PaymentAddress(GetOwner(), aCountry, aAddressLine, aRegion, + aRegionCode, aCity, aDependentLocality, aPostalCode, + aSortingCode, aOrganization, aRecipient, aPhone); + // Fire shippingaddresschange event + return DispatchUpdateEvent(u"shippingaddresschange"_ns); +} + +void PaymentRequest::SetShippingOption(const nsAString& aShippingOption) { + mShippingOption = aShippingOption; +} + +void PaymentRequest::GetShippingOption(nsAString& aRetVal) const { + aRetVal = mShippingOption; +} + +nsresult PaymentRequest::UpdateShippingOption( + const nsAString& aShippingOption) { + mShippingOption = aShippingOption; + + // Fire shippingaddresschange event + return DispatchUpdateEvent(u"shippingoptionchange"_ns); +} + +nsresult PaymentRequest::UpdatePaymentMethod( + const nsAString& aMethodName, const ChangeDetails& aMethodDetails) { + return DispatchPaymentMethodChangeEvent(aMethodName, aMethodDetails); +} + +void PaymentRequest::SetShippingType( + const Nullable<PaymentShippingType>& aShippingType) { + mShippingType = aShippingType; +} + +Nullable<PaymentShippingType> PaymentRequest::GetShippingType() const { + return mShippingType; +} + +void PaymentRequest::GetOptions(PaymentOptions& aRetVal) const { + aRetVal = mOptions; +} + +void PaymentRequest::SetOptions(const PaymentOptions& aOptions) { + mOptions = aOptions; +} + +void PaymentRequest::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + return; + } + + MOZ_ASSERT(aCx); + mUpdating = false; + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + ErrorResult result; + // Converting value to a PaymentDetailsUpdate dictionary + RootedDictionary<PaymentDetailsUpdate> details(aCx); + if (!details.Init(aCx, aValue)) { + result.StealExceptionFromJSContext(aCx); + AbortUpdate(result); + return; + } + + IsValidDetailsUpdate(details, mRequestShipping, result); + if (result.Failed()) { + AbortUpdate(result); + return; + } + + // Update the PaymentRequest with the new details + UpdatePayment(aCx, details, result); + if (result.Failed()) { + AbortUpdate(result); + return; + } +} + +void PaymentRequest::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!InFullyActiveDocument()) { + return; + } + + mUpdating = false; + ErrorResult result; + result.ThrowAbortError( + "Details promise for PaymentRequest.show() is rejected by merchant"); + AbortUpdate(result); +} + +bool PaymentRequest::InFullyActiveDocument() { + nsIGlobalObject* global = GetOwnerGlobal(); + if (!global) { + return false; + } + + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global); + + Document* doc = win->GetExtantDoc(); + if (!doc || !doc->IsCurrentActiveDocument()) { + return false; + } + + WindowContext* winContext = win->GetWindowContext(); + if (!winContext) { + return false; + } + + while (winContext) { + if (!winContext->IsCurrent()) { + return false; + } + winContext = winContext->GetParentWindowContext(); + } + + return true; +} + +void PaymentRequest::RegisterActivityObserver() { + if (nsPIDOMWindowInner* window = GetOwner()) { + mDocument = window->GetExtantDoc(); + if (mDocument) { + mDocument->RegisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } + } +} + +void PaymentRequest::UnregisterActivityObserver() { + if (mDocument) { + mDocument->UnregisterActivityObserver( + NS_ISUPPORTS_CAST(nsIDocumentActivity*, this)); + } +} + +void PaymentRequest::NotifyOwnerDocumentActivityChanged() { + nsPIDOMWindowInner* window = GetOwner(); + NS_ENSURE_TRUE_VOID(window); + Document* doc = window->GetExtantDoc(); + NS_ENSURE_TRUE_VOID(doc); + + if (!InFullyActiveDocument()) { + if (mState == eInteractive) { + if (mAcceptPromise) { + mAcceptPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mAcceptPromise = nullptr; + } + if (mResponse) { + ErrorResult rejectReason; + rejectReason.ThrowAbortError("The owner documnet is not fully active"); + mResponse->RejectRetry(std::move(rejectReason)); + } + if (mAbortPromise) { + mAbortPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mAbortPromise = nullptr; + } + } + if (mState == eCreated) { + if (mResultPromise) { + mResultPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + mResultPromise = nullptr; + } + } + RefPtr<PaymentRequestManager> mgr = PaymentRequestManager::GetSingleton(); + mgr->ClosePayment(this); + } +} + +PaymentRequest::~PaymentRequest() { + // Suppress any pending unreported exception on mUpdateError. We don't use + // IgnoredErrorResult for mUpdateError because that doesn't play very nice + // with move assignment operators. + mUpdateError.SuppressException(); + + if (mIPC) { + // If we're being destroyed, the PaymentRequestManager isn't holding any + // references to us and we can't be waiting for any replies. + mIPC->MaybeDelete(false); + } + UnregisterActivityObserver(); +} + +JSObject* PaymentRequest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaymentRequest_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentRequest.h b/dom/payments/PaymentRequest.h new file mode 100644 index 0000000000..9c9f2f4065 --- /dev/null +++ b/dom/payments/PaymentRequest.h @@ -0,0 +1,280 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequest_h +#define mozilla_dom_PaymentRequest_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/PaymentRequestBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/ErrorResult.h" +#include "nsIDocumentActivity.h" +#include "nsWrapperCache.h" +#include "PaymentRequestUpdateEvent.h" + +namespace mozilla::dom { + +class PaymentAddress; +class PaymentRequestChild; +class PaymentResponse; +class ResponseData; + +class GeneralDetails final { + public: + GeneralDetails() = default; + ~GeneralDetails() = default; + nsString details; +}; + +class BasicCardDetails final { + public: + struct Address { + nsString country; + CopyableTArray<nsString> addressLine; + nsString region; + nsString regionCode; + nsString city; + nsString dependentLocality; + nsString postalCode; + nsString sortingCode; + nsString organization; + nsString recipient; + nsString phone; + }; + BasicCardDetails() = default; + ~BasicCardDetails() = default; + + Address billingAddress; +}; + +class ChangeDetails final { + public: + enum Type { Unknown = 0, GeneralMethodDetails = 1, BasicCardMethodDetails }; + ChangeDetails() : mType(ChangeDetails::Unknown) {} + explicit ChangeDetails(const GeneralDetails& aGeneralDetails) + : mType(GeneralMethodDetails), mGeneralDetails(aGeneralDetails) {} + explicit ChangeDetails(const BasicCardDetails& aBasicCardDetails) + : mType(BasicCardMethodDetails), mBasicCardDetails(aBasicCardDetails) {} + ChangeDetails& operator=(const GeneralDetails& aGeneralDetails) { + mType = GeneralMethodDetails; + mGeneralDetails = aGeneralDetails; + mBasicCardDetails = BasicCardDetails(); + return *this; + } + ChangeDetails& operator=(const BasicCardDetails& aBasicCardDetails) { + mType = BasicCardMethodDetails; + mGeneralDetails = GeneralDetails(); + mBasicCardDetails = aBasicCardDetails; + return *this; + } + ~ChangeDetails() = default; + + const Type& type() const { return mType; } + const GeneralDetails& generalDetails() const { return mGeneralDetails; } + const BasicCardDetails& basicCardDetails() const { return mBasicCardDetails; } + + private: + Type mType; + GeneralDetails mGeneralDetails; + BasicCardDetails mBasicCardDetails; +}; + +class PaymentRequest final : public DOMEventTargetHelper, + public PromiseNativeHandler, + public nsIDocumentActivity { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PaymentRequest, + DOMEventTargetHelper) + NS_DECL_NSIDOCUMENTACTIVITY + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<PaymentRequest> CreatePaymentRequest( + nsPIDOMWindowInner* aWindow, ErrorResult& aRv); + + static bool PrefEnabled(JSContext* aCx, JSObject* aObj); + + // Parameter validation methods + static void IsValidStandardizedPMI(const nsAString& aIdentifier, + ErrorResult& aRv); + + static void IsValidPaymentMethodIdentifier(const nsAString& aIdentifier, + ErrorResult& aRv); + + static void IsValidMethodData(JSContext* aCx, + const Sequence<PaymentMethodData>& aMethodData, + ErrorResult& aRv); + + static void IsValidNumber(const nsAString& aItem, const nsAString& aStr, + ErrorResult& aRv); + + static void IsNonNegativeNumber(const nsAString& aItem, const nsAString& aStr, + ErrorResult& aRv); + + static void IsValidCurrencyAmount(const nsAString& aItem, + const PaymentCurrencyAmount& aAmount, + const bool aIsTotalItem, ErrorResult& aRv); + + static void IsValidCurrency(const nsAString& aItem, + const nsAString& aCurrency, ErrorResult& aRv); + + static void IsValidDetailsInit(const PaymentDetailsInit& aDetails, + const bool aRequestShipping, ErrorResult& aRv); + + static void IsValidDetailsUpdate(const PaymentDetailsUpdate& aDetails, + const bool aRequestShipping, + ErrorResult& aRv); + + static void IsValidDetailsBase(const PaymentDetailsBase& aDetails, + const bool aRequestShipping, ErrorResult& aRv); + + // Webidl implementation + static already_AddRefed<PaymentRequest> Constructor( + const GlobalObject& aGlobal, + const Sequence<PaymentMethodData>& aMethodData, + const PaymentDetailsInit& aDetails, const PaymentOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed<Promise> CanMakePayment(ErrorResult& aRv); + void RespondCanMakePayment(bool aResult); + + already_AddRefed<Promise> Show( + const Optional<OwningNonNull<Promise>>& detailsPromise, ErrorResult& aRv); + void RespondShowPayment(const nsAString& aMethodName, + const ResponseData& aData, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone, ErrorResult&& aResult); + void RejectShowPayment(ErrorResult&& aRejectReason); + void RespondComplete(); + + already_AddRefed<Promise> Abort(ErrorResult& aRv); + void RespondAbortPayment(bool aResult); + + void RetryPayment(JSContext* aCx, const PaymentValidationErrors& aErrors, + ErrorResult& aRv); + + void GetId(nsAString& aRetVal) const; + void GetInternalId(nsAString& aRetVal); + void SetId(const nsAString& aId); + + bool Equals(const nsAString& aInternalId) const; + + bool ReadyForUpdate(); + bool IsUpdating() const { return mUpdating; } + void SetUpdating(bool aUpdating); + + already_AddRefed<PaymentResponse> GetResponse() const; + + already_AddRefed<PaymentAddress> GetShippingAddress() const; + // Update mShippingAddress and fire shippingaddresschange event + nsresult UpdateShippingAddress( + const nsAString& aCountry, const nsTArray<nsString>& aAddressLine, + const nsAString& aRegion, const nsAString& aRegionCode, + const nsAString& aCity, const nsAString& aDependentLocality, + const nsAString& aPostalCode, const nsAString& aSortingCode, + const nsAString& aOrganization, const nsAString& aRecipient, + const nsAString& aPhone); + + void SetShippingOption(const nsAString& aShippingOption); + void GetShippingOption(nsAString& aRetVal) const; + void GetOptions(PaymentOptions& aRetVal) const; + void SetOptions(const PaymentOptions& aOptions); + nsresult UpdateShippingOption(const nsAString& aShippingOption); + + void UpdatePayment(JSContext* aCx, const PaymentDetailsUpdate& aDetails, + ErrorResult& aRv); + void AbortUpdate(ErrorResult& aReason); + + void SetShippingType(const Nullable<PaymentShippingType>& aShippingType); + Nullable<PaymentShippingType> GetShippingType() const; + + inline void ShippingWasRequested() { mRequestShipping = true; } + + nsresult UpdatePaymentMethod(const nsAString& aMethodName, + const ChangeDetails& aMethodDetails); + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + bool InFullyActiveDocument(); + + IMPL_EVENT_HANDLER(merchantvalidation); + IMPL_EVENT_HANDLER(shippingaddresschange); + IMPL_EVENT_HANDLER(shippingoptionchange); + IMPL_EVENT_HANDLER(paymentmethodchange); + + void SetIPC(PaymentRequestChild* aChild) { mIPC = aChild; } + + PaymentRequestChild* GetIPC() const { return mIPC; } + + private: + PaymentOptions mOptions; + + protected: + ~PaymentRequest(); + + void RegisterActivityObserver(); + void UnregisterActivityObserver(); + + nsresult DispatchUpdateEvent(const nsAString& aType); + + nsresult DispatchMerchantValidationEvent(const nsAString& aType); + + nsresult DispatchPaymentMethodChangeEvent(const nsAString& aMethodName, + const ChangeDetails& aMethodDatils); + + PaymentRequest(nsPIDOMWindowInner* aWindow, const nsAString& aInternalId); + + // Id for internal identification + nsString mInternalId; + // Id for communicating with merchant side + // mId is initialized to details.id if it exists + // otherwise, mId has the same value as mInternalId. + nsString mId; + // Promise for "PaymentRequest::CanMakePayment" + RefPtr<Promise> mResultPromise; + // Promise for "PaymentRequest::Show" + RefPtr<Promise> mAcceptPromise; + // Promise for "PaymentRequest::Abort" + RefPtr<Promise> mAbortPromise; + // Resolve mAcceptPromise with mResponse if user accepts the request. + RefPtr<PaymentResponse> mResponse; + // The redacted shipping address. + RefPtr<PaymentAddress> mShippingAddress; + // The full shipping address to be used in the response upon payment. + RefPtr<PaymentAddress> mFullShippingAddress; + // Hold a reference to the document to allow unregistering the activity + // observer. + RefPtr<Document> mDocument; + // It is populated when the user chooses a shipping option. + nsString mShippingOption; + + Nullable<PaymentShippingType> mShippingType; + + // "true" when there is a pending updateWith() call to update the payment + // request and "false" otherwise. + bool mUpdating; + + // Whether shipping was requested. This models [[options]].requestShipping, + // but we don't actually store the full [[options]] internal slot. + bool mRequestShipping; + + // The error is set in AbortUpdate(). The value is not-failed by default. + ErrorResult mUpdateError; + + enum { eUnknown, eCreated, eInteractive, eClosed } mState; + + PaymentRequestChild* mIPC; +}; +} // namespace mozilla::dom + +#endif // mozilla_dom_PaymentRequest_h diff --git a/dom/payments/PaymentRequestData.cpp b/dom/payments/PaymentRequestData.cpp new file mode 100644 index 0000000000..4bf3699f2e --- /dev/null +++ b/dom/payments/PaymentRequestData.cpp @@ -0,0 +1,809 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PaymentRequestBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIMutableArray.h" +#include "nsUnicharUtils.h" +#include "PaymentRequestData.h" +#include "PaymentRequestUtils.h" + +namespace mozilla::dom::payments { + +/* PaymentMethodData */ + +NS_IMPL_ISUPPORTS(PaymentMethodData, nsIPaymentMethodData) + +PaymentMethodData::PaymentMethodData(const nsAString& aSupportedMethods, + const nsAString& aData) + : mSupportedMethods(aSupportedMethods), mData(aData) {} + +nsresult PaymentMethodData::Create(const IPCPaymentMethodData& aIPCMethodData, + nsIPaymentMethodData** aMethodData) { + NS_ENSURE_ARG_POINTER(aMethodData); + nsCOMPtr<nsIPaymentMethodData> methodData = new PaymentMethodData( + aIPCMethodData.supportedMethods(), aIPCMethodData.data()); + methodData.forget(aMethodData); + return NS_OK; +} + +NS_IMETHODIMP +PaymentMethodData::GetSupportedMethods(nsAString& aSupportedMethods) { + aSupportedMethods = mSupportedMethods; + return NS_OK; +} + +NS_IMETHODIMP +PaymentMethodData::GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData) { + if (mData.IsEmpty()) { + aData.set(JS::NullValue()); + return NS_OK; + } + nsresult rv = DeserializeToJSValue(mData, aCx, aData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +/* PaymentCurrencyAmount */ + +NS_IMPL_ISUPPORTS(PaymentCurrencyAmount, nsIPaymentCurrencyAmount) + +PaymentCurrencyAmount::PaymentCurrencyAmount(const nsAString& aCurrency, + const nsAString& aValue) + : mValue(aValue) { + /* + * According to the spec + * https://w3c.github.io/payment-request/#validity-checkers + * Set amount.currency to the result of ASCII uppercasing amount.currency. + */ + ToUpperCase(aCurrency, mCurrency); +} + +nsresult PaymentCurrencyAmount::Create( + const IPCPaymentCurrencyAmount& aIPCAmount, + nsIPaymentCurrencyAmount** aAmount) { + NS_ENSURE_ARG_POINTER(aAmount); + nsCOMPtr<nsIPaymentCurrencyAmount> amount = + new PaymentCurrencyAmount(aIPCAmount.currency(), aIPCAmount.value()); + amount.forget(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +PaymentCurrencyAmount::GetCurrency(nsAString& aCurrency) { + aCurrency = mCurrency; + return NS_OK; +} + +NS_IMETHODIMP +PaymentCurrencyAmount::GetValue(nsAString& aValue) { + aValue = mValue; + return NS_OK; +} + +/* PaymentItem */ + +NS_IMPL_ISUPPORTS(PaymentItem, nsIPaymentItem) + +PaymentItem::PaymentItem(const nsAString& aLabel, + nsIPaymentCurrencyAmount* aAmount, const bool aPending) + : mLabel(aLabel), mAmount(aAmount), mPending(aPending) {} + +nsresult PaymentItem::Create(const IPCPaymentItem& aIPCItem, + nsIPaymentItem** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + nsCOMPtr<nsIPaymentCurrencyAmount> amount; + nsresult rv = + PaymentCurrencyAmount::Create(aIPCItem.amount(), getter_AddRefs(amount)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsIPaymentItem> item = + new PaymentItem(aIPCItem.label(), amount, aIPCItem.pending()); + item.forget(aItem); + return NS_OK; +} + +NS_IMETHODIMP +PaymentItem::GetLabel(nsAString& aLabel) { + aLabel = mLabel; + return NS_OK; +} + +NS_IMETHODIMP +PaymentItem::GetAmount(nsIPaymentCurrencyAmount** aAmount) { + NS_ENSURE_ARG_POINTER(aAmount); + MOZ_ASSERT(mAmount); + nsCOMPtr<nsIPaymentCurrencyAmount> amount = mAmount; + amount.forget(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +PaymentItem::GetPending(bool* aPending) { + NS_ENSURE_ARG_POINTER(aPending); + *aPending = mPending; + return NS_OK; +} + +/* PaymentDetailsModifier */ + +NS_IMPL_ISUPPORTS(PaymentDetailsModifier, nsIPaymentDetailsModifier) + +PaymentDetailsModifier::PaymentDetailsModifier( + const nsAString& aSupportedMethods, nsIPaymentItem* aTotal, + nsIArray* aAdditionalDisplayItems, const nsAString& aData) + : mSupportedMethods(aSupportedMethods), + mTotal(aTotal), + mAdditionalDisplayItems(aAdditionalDisplayItems), + mData(aData) {} + +nsresult PaymentDetailsModifier::Create( + const IPCPaymentDetailsModifier& aIPCModifier, + nsIPaymentDetailsModifier** aModifier) { + NS_ENSURE_ARG_POINTER(aModifier); + nsCOMPtr<nsIPaymentItem> total; + nsresult rv = + PaymentItem::Create(aIPCModifier.total(), getter_AddRefs(total)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIArray> displayItems; + if (aIPCModifier.additionalDisplayItemsPassed()) { + nsCOMPtr<nsIMutableArray> items = do_CreateInstance(NS_ARRAY_CONTRACTID); + MOZ_ASSERT(items); + for (const IPCPaymentItem& item : aIPCModifier.additionalDisplayItems()) { + nsCOMPtr<nsIPaymentItem> additionalItem; + rv = PaymentItem::Create(item, getter_AddRefs(additionalItem)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = items->AppendElement(additionalItem); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + displayItems = std::move(items); + } + nsCOMPtr<nsIPaymentDetailsModifier> modifier = + new PaymentDetailsModifier(aIPCModifier.supportedMethods(), total, + displayItems, aIPCModifier.data()); + modifier.forget(aModifier); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetailsModifier::GetSupportedMethods(nsAString& aSupportedMethods) { + aSupportedMethods = mSupportedMethods; + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetailsModifier::GetTotal(nsIPaymentItem** aTotal) { + NS_ENSURE_ARG_POINTER(aTotal); + MOZ_ASSERT(mTotal); + nsCOMPtr<nsIPaymentItem> total = mTotal; + total.forget(aTotal); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetailsModifier::GetAdditionalDisplayItems( + nsIArray** aAdditionalDisplayItems) { + NS_ENSURE_ARG_POINTER(aAdditionalDisplayItems); + nsCOMPtr<nsIArray> additionalItems = mAdditionalDisplayItems; + additionalItems.forget(aAdditionalDisplayItems); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetailsModifier::GetData(JSContext* aCx, + JS::MutableHandle<JS::Value> aData) { + if (mData.IsEmpty()) { + aData.set(JS::NullValue()); + return NS_OK; + } + nsresult rv = DeserializeToJSValue(mData, aCx, aData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +/* PaymentShippingOption */ + +NS_IMPL_ISUPPORTS(PaymentShippingOption, nsIPaymentShippingOption) + +PaymentShippingOption::PaymentShippingOption(const nsAString& aId, + const nsAString& aLabel, + nsIPaymentCurrencyAmount* aAmount, + const bool aSelected) + : mId(aId), mLabel(aLabel), mAmount(aAmount), mSelected(aSelected) {} + +nsresult PaymentShippingOption::Create( + const IPCPaymentShippingOption& aIPCOption, + nsIPaymentShippingOption** aOption) { + NS_ENSURE_ARG_POINTER(aOption); + nsCOMPtr<nsIPaymentCurrencyAmount> amount; + nsresult rv = PaymentCurrencyAmount::Create(aIPCOption.amount(), + getter_AddRefs(amount)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsIPaymentShippingOption> option = new PaymentShippingOption( + aIPCOption.id(), aIPCOption.label(), amount, aIPCOption.selected()); + option.forget(aOption); + return NS_OK; +} + +NS_IMETHODIMP +PaymentShippingOption::GetId(nsAString& aId) { + aId = mId; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShippingOption::GetLabel(nsAString& aLabel) { + aLabel = mLabel; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShippingOption::GetAmount(nsIPaymentCurrencyAmount** aAmount) { + NS_ENSURE_ARG_POINTER(aAmount); + MOZ_ASSERT(mAmount); + nsCOMPtr<nsIPaymentCurrencyAmount> amount = mAmount; + amount.forget(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +PaymentShippingOption::GetSelected(bool* aSelected) { + NS_ENSURE_ARG_POINTER(aSelected); + *aSelected = mSelected; + return NS_OK; +} + +NS_IMETHODIMP +PaymentShippingOption::SetSelected(bool aSelected) { + mSelected = aSelected; + return NS_OK; +} + +/* PaymentDetails */ + +NS_IMPL_ISUPPORTS(PaymentDetails, nsIPaymentDetails) + +PaymentDetails::PaymentDetails(const nsAString& aId, nsIPaymentItem* aTotalItem, + nsIArray* aDisplayItems, + nsIArray* aShippingOptions, nsIArray* aModifiers, + const nsAString& aError, + const nsAString& aShippingAddressErrors, + const nsAString& aPayerErrors, + const nsAString& aPaymentMethodErrors) + : mId(aId), + mTotalItem(aTotalItem), + mDisplayItems(aDisplayItems), + mShippingOptions(aShippingOptions), + mModifiers(aModifiers), + mError(aError), + mShippingAddressErrors(aShippingAddressErrors), + mPayerErrors(aPayerErrors), + mPaymentMethodErrors(aPaymentMethodErrors) {} + +nsresult PaymentDetails::Create(const IPCPaymentDetails& aIPCDetails, + nsIPaymentDetails** aDetails) { + NS_ENSURE_ARG_POINTER(aDetails); + + nsCOMPtr<nsIPaymentItem> total; + nsresult rv = PaymentItem::Create(aIPCDetails.total(), getter_AddRefs(total)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIArray> displayItems; + nsCOMPtr<nsIMutableArray> items = do_CreateInstance(NS_ARRAY_CONTRACTID); + MOZ_ASSERT(items); + for (const IPCPaymentItem& displayItem : aIPCDetails.displayItems()) { + nsCOMPtr<nsIPaymentItem> item; + rv = PaymentItem::Create(displayItem, getter_AddRefs(item)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = items->AppendElement(item); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + displayItems = std::move(items); + + nsCOMPtr<nsIArray> shippingOptions; + nsCOMPtr<nsIMutableArray> options = do_CreateInstance(NS_ARRAY_CONTRACTID); + MOZ_ASSERT(options); + for (const IPCPaymentShippingOption& shippingOption : + aIPCDetails.shippingOptions()) { + nsCOMPtr<nsIPaymentShippingOption> option; + rv = PaymentShippingOption::Create(shippingOption, getter_AddRefs(option)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = options->AppendElement(option); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + shippingOptions = std::move(options); + + nsCOMPtr<nsIArray> modifiers; + nsCOMPtr<nsIMutableArray> detailsModifiers = + do_CreateInstance(NS_ARRAY_CONTRACTID); + MOZ_ASSERT(detailsModifiers); + for (const IPCPaymentDetailsModifier& modifier : aIPCDetails.modifiers()) { + nsCOMPtr<nsIPaymentDetailsModifier> detailsModifier; + rv = PaymentDetailsModifier::Create(modifier, + getter_AddRefs(detailsModifier)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = detailsModifiers->AppendElement(detailsModifier); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + modifiers = std::move(detailsModifiers); + + nsCOMPtr<nsIPaymentDetails> details = new PaymentDetails( + aIPCDetails.id(), total, displayItems, shippingOptions, modifiers, + aIPCDetails.error(), aIPCDetails.shippingAddressErrors(), + aIPCDetails.payerErrors(), aIPCDetails.paymentMethodErrors()); + + details.forget(aDetails); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetId(nsAString& aId) { + aId = mId; + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetTotalItem(nsIPaymentItem** aTotalItem) { + NS_ENSURE_ARG_POINTER(aTotalItem); + MOZ_ASSERT(mTotalItem); + nsCOMPtr<nsIPaymentItem> total = mTotalItem; + total.forget(aTotalItem); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetDisplayItems(nsIArray** aDisplayItems) { + NS_ENSURE_ARG_POINTER(aDisplayItems); + nsCOMPtr<nsIArray> displayItems = mDisplayItems; + displayItems.forget(aDisplayItems); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetShippingOptions(nsIArray** aShippingOptions) { + NS_ENSURE_ARG_POINTER(aShippingOptions); + nsCOMPtr<nsIArray> options = mShippingOptions; + options.forget(aShippingOptions); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetModifiers(nsIArray** aModifiers) { + NS_ENSURE_ARG_POINTER(aModifiers); + nsCOMPtr<nsIArray> modifiers = mModifiers; + modifiers.forget(aModifiers); + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetError(nsAString& aError) { + aError = mError; + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetShippingAddressErrors(JSContext* aCx, + JS::MutableHandle<JS::Value> aErrors) { + AddressErrors errors; + errors.Init(mShippingAddressErrors); + if (!ToJSValue(aCx, errors, aErrors)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetPayerErrors(JSContext* aCx, + JS::MutableHandle<JS::Value> aErrors) { + PayerErrors errors; + errors.Init(mPayerErrors); + if (!ToJSValue(aCx, errors, aErrors)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentDetails::GetPaymentMethodErrors(JSContext* aCx, + JS::MutableHandle<JS::Value> aErrors) { + if (mPaymentMethodErrors.IsEmpty()) { + aErrors.set(JS::NullValue()); + return NS_OK; + } + nsresult rv = DeserializeToJSValue(mPaymentMethodErrors, aCx, aErrors); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +nsresult PaymentDetails::Update(nsIPaymentDetails* aDetails, + const bool aRequestShipping) { + MOZ_ASSERT(aDetails); + /* + * According to the spec [1], update the attributes if they present in new + * details (i.e., PaymentDetailsUpdate); otherwise, keep original value. + * Note |id| comes only from initial details (i.e., PaymentDetailsInit) and + * |error| only from new details. + * + * [1] https://www.w3.org/TR/payment-request/#updatewith-method + */ + + nsresult rv = aDetails->GetTotalItem(getter_AddRefs(mTotalItem)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIArray> displayItems; + rv = aDetails->GetDisplayItems(getter_AddRefs(displayItems)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (displayItems) { + mDisplayItems = displayItems; + } + + if (aRequestShipping) { + nsCOMPtr<nsIArray> shippingOptions; + rv = aDetails->GetShippingOptions(getter_AddRefs(shippingOptions)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mShippingOptions = shippingOptions; + } + + nsCOMPtr<nsIArray> modifiers; + rv = aDetails->GetModifiers(getter_AddRefs(modifiers)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (modifiers) { + mModifiers = modifiers; + } + + rv = aDetails->GetError(mError); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PaymentDetails* rowDetails = static_cast<PaymentDetails*>(aDetails); + MOZ_ASSERT(rowDetails); + mShippingAddressErrors = rowDetails->GetShippingAddressErrors(); + mPayerErrors = rowDetails->GetPayerErrors(); + mPaymentMethodErrors = rowDetails->GetPaymentMethodErrors(); + + return NS_OK; +} + +const nsString& PaymentDetails::GetShippingAddressErrors() const { + return mShippingAddressErrors; +} + +const nsString& PaymentDetails::GetPayerErrors() const { return mPayerErrors; } + +const nsString& PaymentDetails::GetPaymentMethodErrors() const { + return mPaymentMethodErrors; +} + +nsresult PaymentDetails::UpdateErrors(const nsAString& aError, + const nsAString& aPayerErrors, + const nsAString& aPaymentMethodErrors, + const nsAString& aShippingAddressErrors) { + mError = aError; + mPayerErrors = aPayerErrors; + mPaymentMethodErrors = aPaymentMethodErrors; + mShippingAddressErrors = aShippingAddressErrors; + return NS_OK; +} + +/* PaymentOptions */ + +NS_IMPL_ISUPPORTS(PaymentOptions, nsIPaymentOptions) + +PaymentOptions::PaymentOptions(const bool aRequestPayerName, + const bool aRequestPayerEmail, + const bool aRequestPayerPhone, + const bool aRequestShipping, + const bool aRequestBillingAddress, + const nsAString& aShippingType) + : mRequestPayerName(aRequestPayerName), + mRequestPayerEmail(aRequestPayerEmail), + mRequestPayerPhone(aRequestPayerPhone), + mRequestShipping(aRequestShipping), + mRequestBillingAddress(aRequestBillingAddress), + mShippingType(aShippingType) {} + +nsresult PaymentOptions::Create(const IPCPaymentOptions& aIPCOptions, + nsIPaymentOptions** aOptions) { + NS_ENSURE_ARG_POINTER(aOptions); + + nsCOMPtr<nsIPaymentOptions> options = new PaymentOptions( + aIPCOptions.requestPayerName(), aIPCOptions.requestPayerEmail(), + aIPCOptions.requestPayerPhone(), aIPCOptions.requestShipping(), + aIPCOptions.requestBillingAddress(), aIPCOptions.shippingType()); + options.forget(aOptions); + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetRequestPayerName(bool* aRequestPayerName) { + NS_ENSURE_ARG_POINTER(aRequestPayerName); + *aRequestPayerName = mRequestPayerName; + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetRequestPayerEmail(bool* aRequestPayerEmail) { + NS_ENSURE_ARG_POINTER(aRequestPayerEmail); + *aRequestPayerEmail = mRequestPayerEmail; + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetRequestPayerPhone(bool* aRequestPayerPhone) { + NS_ENSURE_ARG_POINTER(aRequestPayerPhone); + *aRequestPayerPhone = mRequestPayerPhone; + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetRequestShipping(bool* aRequestShipping) { + NS_ENSURE_ARG_POINTER(aRequestShipping); + *aRequestShipping = mRequestShipping; + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetRequestBillingAddress(bool* aRequestBillingAddress) { + NS_ENSURE_ARG_POINTER(aRequestBillingAddress); + *aRequestBillingAddress = mRequestBillingAddress; + return NS_OK; +} + +NS_IMETHODIMP +PaymentOptions::GetShippingType(nsAString& aShippingType) { + aShippingType = mShippingType; + return NS_OK; +} + +/* PaymentReqeust */ + +NS_IMPL_ISUPPORTS(PaymentRequest, nsIPaymentRequest) + +PaymentRequest::PaymentRequest(const uint64_t aTopOuterWindowId, + const nsAString& aRequestId, + nsIPrincipal* aTopLevelPrincipal, + nsIArray* aPaymentMethods, + nsIPaymentDetails* aPaymentDetails, + nsIPaymentOptions* aPaymentOptions, + const nsAString& aShippingOption) + : mTopOuterWindowId(aTopOuterWindowId), + mRequestId(aRequestId), + mTopLevelPrincipal(aTopLevelPrincipal), + mPaymentMethods(aPaymentMethods), + mPaymentDetails(aPaymentDetails), + mPaymentOptions(aPaymentOptions), + mShippingOption(aShippingOption), + mState(eCreated) {} + +NS_IMETHODIMP +PaymentRequest::GetTopOuterWindowId(uint64_t* aTopOuterWindowId) { + NS_ENSURE_ARG_POINTER(aTopOuterWindowId); + *aTopOuterWindowId = mTopOuterWindowId; + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetTopLevelPrincipal(nsIPrincipal** aTopLevelPrincipal) { + NS_ENSURE_ARG_POINTER(aTopLevelPrincipal); + MOZ_ASSERT(mTopLevelPrincipal); + nsCOMPtr<nsIPrincipal> principal = mTopLevelPrincipal; + principal.forget(aTopLevelPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetRequestId(nsAString& aRequestId) { + aRequestId = mRequestId; + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetPaymentMethods(nsIArray** aPaymentMethods) { + NS_ENSURE_ARG_POINTER(aPaymentMethods); + MOZ_ASSERT(mPaymentMethods); + nsCOMPtr<nsIArray> methods = mPaymentMethods; + methods.forget(aPaymentMethods); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetPaymentDetails(nsIPaymentDetails** aPaymentDetails) { + NS_ENSURE_ARG_POINTER(aPaymentDetails); + MOZ_ASSERT(mPaymentDetails); + nsCOMPtr<nsIPaymentDetails> details = mPaymentDetails; + details.forget(aPaymentDetails); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetPaymentOptions(nsIPaymentOptions** aPaymentOptions) { + NS_ENSURE_ARG_POINTER(aPaymentOptions); + MOZ_ASSERT(mPaymentOptions); + nsCOMPtr<nsIPaymentOptions> options = mPaymentOptions; + options.forget(aPaymentOptions); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequest::GetShippingOption(nsAString& aShippingOption) { + aShippingOption = mShippingOption; + return NS_OK; +} + +nsresult PaymentRequest::UpdatePaymentDetails( + nsIPaymentDetails* aPaymentDetails, const nsAString& aShippingOption) { + MOZ_ASSERT(aPaymentDetails); + bool requestShipping; + nsresult rv = mPaymentOptions->GetRequestShipping(&requestShipping); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mShippingOption = aShippingOption; + + PaymentDetails* rowDetails = + static_cast<PaymentDetails*>(mPaymentDetails.get()); + MOZ_ASSERT(rowDetails); + return rowDetails->Update(aPaymentDetails, requestShipping); +} + +void PaymentRequest::SetCompleteStatus(const nsAString& aCompleteStatus) { + mCompleteStatus = aCompleteStatus; +} + +nsresult PaymentRequest::UpdateErrors(const nsAString& aError, + const nsAString& aPayerErrors, + const nsAString& aPaymentMethodErrors, + const nsAString& aShippingAddressErrors) { + PaymentDetails* rowDetails = + static_cast<PaymentDetails*>(mPaymentDetails.get()); + MOZ_ASSERT(rowDetails); + return rowDetails->UpdateErrors(aError, aPayerErrors, aPaymentMethodErrors, + aShippingAddressErrors); +} + +NS_IMETHODIMP +PaymentRequest::GetCompleteStatus(nsAString& aCompleteStatus) { + aCompleteStatus = mCompleteStatus; + return NS_OK; +} + +/* PaymentAddress */ + +NS_IMPL_ISUPPORTS(PaymentAddress, nsIPaymentAddress) + +NS_IMETHODIMP +PaymentAddress::Init(const nsAString& aCountry, nsIArray* aAddressLine, + const nsAString& aRegion, const nsAString& aRegionCode, + const nsAString& aCity, + const nsAString& aDependentLocality, + const nsAString& aPostalCode, + const nsAString& aSortingCode, + const nsAString& aOrganization, + const nsAString& aRecipient, const nsAString& aPhone) { + mCountry = aCountry; + mAddressLine = aAddressLine; + mRegion = aRegion; + mRegionCode = aRegionCode; + mCity = aCity; + mDependentLocality = aDependentLocality; + mPostalCode = aPostalCode; + mSortingCode = aSortingCode; + mOrganization = aOrganization; + mRecipient = aRecipient; + mPhone = aPhone; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetCountry(nsAString& aCountry) { + aCountry = mCountry; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetAddressLine(nsIArray** aAddressLine) { + NS_ENSURE_ARG_POINTER(aAddressLine); + nsCOMPtr<nsIArray> addressLine = mAddressLine; + addressLine.forget(aAddressLine); + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetRegion(nsAString& aRegion) { + aRegion = mRegion; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetRegionCode(nsAString& aRegionCode) { + aRegionCode = mRegionCode; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetCity(nsAString& aCity) { + aCity = mCity; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetDependentLocality(nsAString& aDependentLocality) { + aDependentLocality = mDependentLocality; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetPostalCode(nsAString& aPostalCode) { + aPostalCode = mPostalCode; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetSortingCode(nsAString& aSortingCode) { + aSortingCode = mSortingCode; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetOrganization(nsAString& aOrganization) { + aOrganization = mOrganization; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetRecipient(nsAString& aRecipient) { + aRecipient = mRecipient; + return NS_OK; +} + +NS_IMETHODIMP +PaymentAddress::GetPhone(nsAString& aPhone) { + aPhone = mPhone; + return NS_OK; +} + +} // namespace mozilla::dom::payments diff --git a/dom/payments/PaymentRequestData.h b/dom/payments/PaymentRequestData.h new file mode 100644 index 0000000000..e4b9cadcd1 --- /dev/null +++ b/dom/payments/PaymentRequestData.h @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestData_h +#define mozilla_dom_PaymentRequestData_h + +#include "nsIPaymentAddress.h" +#include "nsIPaymentRequest.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/PaymentRequestParent.h" + +namespace mozilla::dom::payments { + +class PaymentMethodData final : public nsIPaymentMethodData { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTMETHODDATA + + static nsresult Create(const IPCPaymentMethodData& aIPCMethodData, + nsIPaymentMethodData** aMethodData); + + private: + PaymentMethodData(const nsAString& aSupportedMethods, const nsAString& aData); + + ~PaymentMethodData() = default; + + nsString mSupportedMethods; + nsString mData; +}; + +class PaymentCurrencyAmount final : public nsIPaymentCurrencyAmount { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTCURRENCYAMOUNT + + static nsresult Create(const IPCPaymentCurrencyAmount& aIPCAmount, + nsIPaymentCurrencyAmount** aAmount); + + private: + PaymentCurrencyAmount(const nsAString& aCurrency, const nsAString& aValue); + + ~PaymentCurrencyAmount() = default; + + nsString mCurrency; + nsString mValue; +}; + +class PaymentItem final : public nsIPaymentItem { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTITEM + + static nsresult Create(const IPCPaymentItem& aIPCItem, + nsIPaymentItem** aItem); + + private: + PaymentItem(const nsAString& aLabel, nsIPaymentCurrencyAmount* aAmount, + const bool aPending); + + ~PaymentItem() = default; + + nsString mLabel; + nsCOMPtr<nsIPaymentCurrencyAmount> mAmount; + bool mPending; +}; + +class PaymentDetailsModifier final : public nsIPaymentDetailsModifier { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTDETAILSMODIFIER + + static nsresult Create(const IPCPaymentDetailsModifier& aIPCModifier, + nsIPaymentDetailsModifier** aModifier); + + private: + PaymentDetailsModifier(const nsAString& aSupportedMethods, + nsIPaymentItem* aTotal, + nsIArray* aAdditionalDisplayItems, + const nsAString& aData); + + ~PaymentDetailsModifier() = default; + + nsString mSupportedMethods; + nsCOMPtr<nsIPaymentItem> mTotal; + nsCOMPtr<nsIArray> mAdditionalDisplayItems; + nsString mData; +}; + +class PaymentShippingOption final : public nsIPaymentShippingOption { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTSHIPPINGOPTION + + static nsresult Create(const IPCPaymentShippingOption& aIPCOption, + nsIPaymentShippingOption** aOption); + + private: + PaymentShippingOption(const nsAString& aId, const nsAString& aLabel, + nsIPaymentCurrencyAmount* aAmount, + const bool aSelected = false); + + ~PaymentShippingOption() = default; + + nsString mId; + nsString mLabel; + nsCOMPtr<nsIPaymentCurrencyAmount> mAmount; + bool mSelected; +}; + +class PaymentDetails final : public nsIPaymentDetails { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTDETAILS + + static nsresult Create(const IPCPaymentDetails& aIPCDetails, + nsIPaymentDetails** aDetails); + nsresult Update(nsIPaymentDetails* aDetails, const bool aRequestShipping); + const nsString& GetShippingAddressErrors() const; + const nsString& GetPayerErrors() const; + const nsString& GetPaymentMethodErrors() const; + nsresult UpdateErrors(const nsAString& aError, const nsAString& aPayerErrors, + const nsAString& aPaymentMethodErrors, + const nsAString& aShippingAddressErrors); + + private: + PaymentDetails(const nsAString& aId, nsIPaymentItem* aTotalItem, + nsIArray* aDisplayItems, nsIArray* aShippingOptions, + nsIArray* aModifiers, const nsAString& aError, + const nsAString& aShippingAddressError, + const nsAString& aPayerError, + const nsAString& aPaymentMethodError); + + ~PaymentDetails() = default; + + nsString mId; + nsCOMPtr<nsIPaymentItem> mTotalItem; + nsCOMPtr<nsIArray> mDisplayItems; + nsCOMPtr<nsIArray> mShippingOptions; + nsCOMPtr<nsIArray> mModifiers; + nsString mError; + nsString mShippingAddressErrors; + nsString mPayerErrors; + nsString mPaymentMethodErrors; +}; + +class PaymentOptions final : public nsIPaymentOptions { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTOPTIONS + + static nsresult Create(const IPCPaymentOptions& aIPCOptions, + nsIPaymentOptions** aOptions); + + private: + PaymentOptions(const bool aRequestPayerName, const bool aRequestPayerEmail, + const bool aRequestPayerPhone, const bool aRequestShipping, + const bool aRequestBillingAddress, + const nsAString& aShippintType); + ~PaymentOptions() = default; + + bool mRequestPayerName; + bool mRequestPayerEmail; + bool mRequestPayerPhone; + bool mRequestShipping; + bool mRequestBillingAddress; + nsString mShippingType; +}; + +class PaymentRequest final : public nsIPaymentRequest { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTREQUEST + + PaymentRequest(const uint64_t aTopOuterWindowId, const nsAString& aRequestId, + nsIPrincipal* aPrincipal, nsIArray* aPaymentMethods, + nsIPaymentDetails* aPaymentDetails, + nsIPaymentOptions* aPaymentOptions, + const nsAString& aShippingOption); + + void SetIPC(PaymentRequestParent* aIPC) { mIPC = aIPC; } + + PaymentRequestParent* GetIPC() const { return mIPC; } + + nsresult UpdatePaymentDetails(nsIPaymentDetails* aPaymentDetails, + const nsAString& aShippingOption); + + void SetCompleteStatus(const nsAString& aCompleteStatus); + + nsresult UpdateErrors(const nsAString& aError, const nsAString& aPayerErrors, + const nsAString& aPaymentMethodErrors, + const nsAString& aShippingAddressErrors); + + // The state represents the PaymentRequest's state in the spec. The state is + // not synchronized between content and parent processes. + // eCreated - the state means a PaymentRequest is created when new + // PaymentRequest() is called. This is the initial state. + // eInteractive - When PaymentRequest is requested to show to users, the state + // becomes eInteractive. Under eInteractive state, Payment UI + // pop up and gather the user's information until the user + // accepts or rejects the PaymentRequest. + // eClosed - When the user accepts or rejects the PaymentRequest, the + // state becomes eClosed. Under eClosed state, response from + // Payment UI would not be accepted by PaymentRequestService + // anymore, except the Complete response. + enum eState { eCreated, eInteractive, eClosed }; + + void SetState(const eState aState) { mState = aState; } + + const eState& GetState() const { return mState; } + + private: + ~PaymentRequest() = default; + + uint64_t mTopOuterWindowId; + nsString mRequestId; + nsString mCompleteStatus; + nsCOMPtr<nsIPrincipal> mTopLevelPrincipal; + nsCOMPtr<nsIArray> mPaymentMethods; + nsCOMPtr<nsIPaymentDetails> mPaymentDetails; + nsCOMPtr<nsIPaymentOptions> mPaymentOptions; + nsString mShippingOption; + + // IPC's life cycle should be controlled by IPC mechanism. + // PaymentRequest should not own the reference of it. + PaymentRequestParent* mIPC; + eState mState; +}; + +class PaymentAddress final : public nsIPaymentAddress { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTADDRESS + + PaymentAddress() = default; + + private: + ~PaymentAddress() = default; + + nsString mCountry; + nsCOMPtr<nsIArray> mAddressLine; + nsString mRegion; + nsString mRegionCode; + nsString mCity; + nsString mDependentLocality; + nsString mPostalCode; + nsString mSortingCode; + nsString mOrganization; + nsString mRecipient; + nsString mPhone; +}; + +} // namespace mozilla::dom::payments + +#endif diff --git a/dom/payments/PaymentRequestManager.cpp b/dom/payments/PaymentRequestManager.cpp new file mode 100644 index 0000000000..f615fedf77 --- /dev/null +++ b/dom/payments/PaymentRequestManager.cpp @@ -0,0 +1,742 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/PaymentRequestChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "nsString.h" +#include "nsIPrincipal.h" +#include "nsIPaymentActionResponse.h" +#include "PaymentRequestManager.h" +#include "PaymentRequestUtils.h" +#include "PaymentResponse.h" + +namespace mozilla::dom { +namespace { + +/* + * Following Convert* functions are used for convert PaymentRequest structs + * to transferable structs for IPC. + */ +void ConvertMethodData(JSContext* aCx, const PaymentMethodData& aMethodData, + IPCPaymentMethodData& aIPCMethodData, ErrorResult& aRv) { + MOZ_ASSERT(aCx); + // Convert JSObject to a serialized string + nsAutoString serializedData; + if (aMethodData.mData.WasPassed()) { + JS::Rooted<JSObject*> object(aCx, aMethodData.mData.Value()); + if (NS_WARN_IF( + NS_FAILED(SerializeFromJSObject(aCx, object, serializedData)))) { + aRv.ThrowTypeError( + "The PaymentMethodData.data must be a serializable object"); + return; + } + } + aIPCMethodData = + IPCPaymentMethodData(aMethodData.mSupportedMethods, serializedData); +} + +void ConvertCurrencyAmount(const PaymentCurrencyAmount& aAmount, + IPCPaymentCurrencyAmount& aIPCCurrencyAmount) { + aIPCCurrencyAmount = + IPCPaymentCurrencyAmount(aAmount.mCurrency, aAmount.mValue); +} + +void ConvertItem(const PaymentItem& aItem, IPCPaymentItem& aIPCItem) { + IPCPaymentCurrencyAmount amount; + ConvertCurrencyAmount(aItem.mAmount, amount); + aIPCItem = IPCPaymentItem(aItem.mLabel, amount, aItem.mPending); +} + +void ConvertModifier(JSContext* aCx, const PaymentDetailsModifier& aModifier, + IPCPaymentDetailsModifier& aIPCModifier, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + // Convert JSObject to a serialized string + nsAutoString serializedData; + if (aModifier.mData.WasPassed()) { + JS::Rooted<JSObject*> object(aCx, aModifier.mData.Value()); + if (NS_WARN_IF( + NS_FAILED(SerializeFromJSObject(aCx, object, serializedData)))) { + aRv.ThrowTypeError("The Modifier.data must be a serializable object"); + return; + } + } + + IPCPaymentItem total; + if (aModifier.mTotal.WasPassed()) { + ConvertItem(aModifier.mTotal.Value(), total); + } + + nsTArray<IPCPaymentItem> additionalDisplayItems; + if (aModifier.mAdditionalDisplayItems.WasPassed()) { + for (const PaymentItem& item : aModifier.mAdditionalDisplayItems.Value()) { + IPCPaymentItem displayItem; + ConvertItem(item, displayItem); + additionalDisplayItems.AppendElement(displayItem); + } + } + aIPCModifier = IPCPaymentDetailsModifier( + aModifier.mSupportedMethods, total, additionalDisplayItems, + serializedData, aModifier.mAdditionalDisplayItems.WasPassed()); +} + +void ConvertShippingOption(const PaymentShippingOption& aOption, + IPCPaymentShippingOption& aIPCOption) { + IPCPaymentCurrencyAmount amount; + ConvertCurrencyAmount(aOption.mAmount, amount); + aIPCOption = IPCPaymentShippingOption(aOption.mId, aOption.mLabel, amount, + aOption.mSelected); +} + +void ConvertDetailsBase(JSContext* aCx, const PaymentDetailsBase& aDetails, + nsTArray<IPCPaymentItem>& aDisplayItems, + nsTArray<IPCPaymentShippingOption>& aShippingOptions, + nsTArray<IPCPaymentDetailsModifier>& aModifiers, + bool aRequestShipping, ErrorResult& aRv) { + MOZ_ASSERT(aCx); + if (aDetails.mDisplayItems.WasPassed()) { + for (const PaymentItem& item : aDetails.mDisplayItems.Value()) { + IPCPaymentItem displayItem; + ConvertItem(item, displayItem); + aDisplayItems.AppendElement(displayItem); + } + } + if (aRequestShipping && aDetails.mShippingOptions.WasPassed()) { + for (const PaymentShippingOption& option : + aDetails.mShippingOptions.Value()) { + IPCPaymentShippingOption shippingOption; + ConvertShippingOption(option, shippingOption); + aShippingOptions.AppendElement(shippingOption); + } + } + if (aDetails.mModifiers.WasPassed()) { + for (const PaymentDetailsModifier& modifier : aDetails.mModifiers.Value()) { + IPCPaymentDetailsModifier detailsModifier; + ConvertModifier(aCx, modifier, detailsModifier, aRv); + if (aRv.Failed()) { + return; + } + aModifiers.AppendElement(detailsModifier); + } + } +} + +void ConvertDetailsInit(JSContext* aCx, const PaymentDetailsInit& aDetails, + IPCPaymentDetails& aIPCDetails, bool aRequestShipping, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + // Convert PaymentDetailsBase members + nsTArray<IPCPaymentItem> displayItems; + nsTArray<IPCPaymentShippingOption> shippingOptions; + nsTArray<IPCPaymentDetailsModifier> modifiers; + ConvertDetailsBase(aCx, aDetails, displayItems, shippingOptions, modifiers, + aRequestShipping, aRv); + if (aRv.Failed()) { + return; + } + + // Convert |id| + nsAutoString id; + if (aDetails.mId.WasPassed()) { + id = aDetails.mId.Value(); + } + + // Convert required |total| + IPCPaymentItem total; + ConvertItem(aDetails.mTotal, total); + + aIPCDetails = + IPCPaymentDetails(id, total, displayItems, shippingOptions, modifiers, + u""_ns, // error message + u""_ns, // shippingAddressErrors + u""_ns, // payerErrors + u""_ns); // paymentMethodErrors +} + +void ConvertDetailsUpdate(JSContext* aCx, const PaymentDetailsUpdate& aDetails, + IPCPaymentDetails& aIPCDetails, bool aRequestShipping, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + // Convert PaymentDetailsBase members + nsTArray<IPCPaymentItem> displayItems; + nsTArray<IPCPaymentShippingOption> shippingOptions; + nsTArray<IPCPaymentDetailsModifier> modifiers; + ConvertDetailsBase(aCx, aDetails, displayItems, shippingOptions, modifiers, + aRequestShipping, aRv); + if (aRv.Failed()) { + return; + } + + // Convert required |total| + IPCPaymentItem total; + if (aDetails.mTotal.WasPassed()) { + ConvertItem(aDetails.mTotal.Value(), total); + } + + // Convert |error| + nsAutoString error; + if (aDetails.mError.WasPassed()) { + error = aDetails.mError.Value(); + } + + nsAutoString shippingAddressErrors; + if (aDetails.mShippingAddressErrors.WasPassed()) { + if (!aDetails.mShippingAddressErrors.Value().ToJSON( + shippingAddressErrors)) { + aRv.ThrowTypeError("The ShippingAddressErrors can not be serailized"); + return; + } + } + + nsAutoString payerErrors; + if (aDetails.mPayerErrors.WasPassed()) { + if (!aDetails.mPayerErrors.Value().ToJSON(payerErrors)) { + aRv.ThrowTypeError("The PayerErrors can not be serialized"); + return; + } + } + + nsAutoString paymentMethodErrors; + if (aDetails.mPaymentMethodErrors.WasPassed()) { + JS::Rooted<JSObject*> object(aCx, aDetails.mPaymentMethodErrors.Value()); + if (NS_WARN_IF(NS_FAILED( + SerializeFromJSObject(aCx, object, paymentMethodErrors)))) { + aRv.ThrowTypeError("The PaymentMethodErrors can not be serialized"); + return; + } + } + + aIPCDetails = IPCPaymentDetails(u""_ns, // id + total, displayItems, shippingOptions, + modifiers, error, shippingAddressErrors, + payerErrors, paymentMethodErrors); +} + +void ConvertOptions(const PaymentOptions& aOptions, + IPCPaymentOptions& aIPCOption) { + NS_ConvertASCIItoUTF16 shippingType( + PaymentShippingTypeValues::GetString(aOptions.mShippingType)); + aIPCOption = + IPCPaymentOptions(aOptions.mRequestPayerName, aOptions.mRequestPayerEmail, + aOptions.mRequestPayerPhone, aOptions.mRequestShipping, + aOptions.mRequestBillingAddress, shippingType); +} + +void ConvertResponseData(const IPCPaymentResponseData& aIPCData, + ResponseData& aData) { + switch (aIPCData.type()) { + case IPCPaymentResponseData::TIPCGeneralResponse: { + const IPCGeneralResponse& data = aIPCData; + GeneralData gData; + gData.data = data.data(); + aData = gData; + break; + } + case IPCPaymentResponseData::TIPCBasicCardResponse: { + const IPCBasicCardResponse& data = aIPCData; + BasicCardData bData; + bData.cardholderName = data.cardholderName(); + bData.cardNumber = data.cardNumber(); + bData.expiryMonth = data.expiryMonth(); + bData.expiryYear = data.expiryYear(); + bData.cardSecurityCode = data.cardSecurityCode(); + bData.billingAddress.country = data.billingAddress().country(); + bData.billingAddress.addressLine = + data.billingAddress().addressLine().Clone(); + bData.billingAddress.region = data.billingAddress().region(); + bData.billingAddress.regionCode = data.billingAddress().regionCode(); + bData.billingAddress.city = data.billingAddress().city(); + bData.billingAddress.dependentLocality = + data.billingAddress().dependentLocality(); + bData.billingAddress.postalCode = data.billingAddress().postalCode(); + bData.billingAddress.sortingCode = data.billingAddress().sortingCode(); + bData.billingAddress.organization = data.billingAddress().organization(); + bData.billingAddress.recipient = data.billingAddress().recipient(); + bData.billingAddress.phone = data.billingAddress().phone(); + aData = bData; + break; + } + default: { + break; + } + } +} + +void ConvertMethodChangeDetails(const IPCMethodChangeDetails& aIPCDetails, + ChangeDetails& aDetails) { + switch (aIPCDetails.type()) { + case IPCMethodChangeDetails::TIPCGeneralChangeDetails: { + const IPCGeneralChangeDetails& details = aIPCDetails; + GeneralDetails gDetails; + gDetails.details = details.details(); + aDetails = gDetails; + break; + } + case IPCMethodChangeDetails::TIPCBasicCardChangeDetails: { + const IPCBasicCardChangeDetails& details = aIPCDetails; + BasicCardDetails bDetails; + bDetails.billingAddress.country = details.billingAddress().country(); + bDetails.billingAddress.addressLine = + details.billingAddress().addressLine(); + bDetails.billingAddress.region = details.billingAddress().region(); + bDetails.billingAddress.regionCode = + details.billingAddress().regionCode(); + bDetails.billingAddress.city = details.billingAddress().city(); + bDetails.billingAddress.dependentLocality = + details.billingAddress().dependentLocality(); + bDetails.billingAddress.postalCode = + details.billingAddress().postalCode(); + bDetails.billingAddress.sortingCode = + details.billingAddress().sortingCode(); + bDetails.billingAddress.organization = + details.billingAddress().organization(); + bDetails.billingAddress.recipient = details.billingAddress().recipient(); + bDetails.billingAddress.phone = details.billingAddress().phone(); + aDetails = bDetails; + break; + } + default: { + break; + } + } +} +} // end of namespace + +/* PaymentRequestManager */ + +StaticRefPtr<PaymentRequestManager> gPaymentManager; +const char kSupportedRegionsPref[] = "dom.payments.request.supportedRegions"; + +void SupportedRegionsPrefChangedCallback(const char* aPrefName, void* aRetval) { + auto retval = static_cast<nsTArray<nsString>*>(aRetval); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kSupportedRegionsPref)); + + nsAutoString supportedRegions; + Preferences::GetString(aPrefName, supportedRegions); + retval->Clear(); + for (const nsAString& each : supportedRegions.Split(',')) { + retval->AppendElement(each); + } +} + +PaymentRequestManager::PaymentRequestManager() { + Preferences::RegisterCallbackAndCall(SupportedRegionsPrefChangedCallback, + kSupportedRegionsPref, + &this->mSupportedRegions); +} + +PaymentRequestManager::~PaymentRequestManager() { + MOZ_ASSERT(mActivePayments.Count() == 0); + Preferences::UnregisterCallback(SupportedRegionsPrefChangedCallback, + kSupportedRegionsPref, + &this->mSupportedRegions); + mSupportedRegions.Clear(); +} + +bool PaymentRequestManager::IsRegionSupported(const nsAString& region) const { + return mSupportedRegions.Contains(region); +} + +PaymentRequestChild* PaymentRequestManager::GetPaymentChild( + PaymentRequest* aRequest) { + MOZ_ASSERT(aRequest); + + if (PaymentRequestChild* child = aRequest->GetIPC()) { + return child; + } + + nsPIDOMWindowInner* win = aRequest->GetOwner(); + NS_ENSURE_TRUE(win, nullptr); + BrowserChild* browserChild = BrowserChild::GetFrom(win->GetDocShell()); + NS_ENSURE_TRUE(browserChild, nullptr); + nsAutoString requestId; + aRequest->GetInternalId(requestId); + + PaymentRequestChild* paymentChild = new PaymentRequestChild(aRequest); + browserChild->SendPPaymentRequestConstructor(paymentChild); + + return paymentChild; +} + +nsresult PaymentRequestManager::SendRequestPayment( + PaymentRequest* aRequest, const IPCPaymentActionRequest& aAction, + bool aResponseExpected) { + PaymentRequestChild* requestChild = GetPaymentChild(aRequest); + // bug 1580496, ignoring the case that requestChild is nullptr. It could be + // nullptr while the corresponding nsPIDOMWindowInner is nullptr. + if (NS_WARN_IF(!requestChild)) { + return NS_ERROR_FAILURE; + } + nsresult rv = requestChild->RequestPayment(aAction); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aResponseExpected) { + ++mActivePayments.LookupOrInsert(aRequest, 0); + } + return NS_OK; +} + +void PaymentRequestManager::NotifyRequestDone(PaymentRequest* aRequest) { + auto entry = mActivePayments.Lookup(aRequest); + MOZ_ASSERT(entry); + MOZ_ASSERT(entry.Data() > 0); + + uint32_t count = --entry.Data(); + if (count == 0) { + entry.Remove(); + } +} + +void PaymentRequestManager::RequestIPCOver(PaymentRequest* aRequest) { + // This must only be called from ActorDestroy or if we're sure we won't + // receive any more IPC for aRequest. + mActivePayments.Remove(aRequest); +} + +already_AddRefed<PaymentRequestManager> PaymentRequestManager::GetSingleton() { + if (!gPaymentManager) { + gPaymentManager = new PaymentRequestManager(); + ClearOnShutdown(&gPaymentManager); + } + RefPtr<PaymentRequestManager> manager = gPaymentManager; + return manager.forget(); +} + +void GetSelectedShippingOption(const PaymentDetailsBase& aDetails, + nsAString& aOption) { + SetDOMStringToNull(aOption); + if (!aDetails.mShippingOptions.WasPassed()) { + return; + } + + const Sequence<PaymentShippingOption>& shippingOptions = + aDetails.mShippingOptions.Value(); + for (const PaymentShippingOption& shippingOption : shippingOptions) { + // set aOption to last selected option's ID + if (shippingOption.mSelected) { + aOption = shippingOption.mId; + } + } +} + +void PaymentRequestManager::CreatePayment( + JSContext* aCx, nsPIDOMWindowInner* aWindow, + nsIPrincipal* aTopLevelPrincipal, + const Sequence<PaymentMethodData>& aMethodData, + const PaymentDetailsInit& aDetails, const PaymentOptions& aOptions, + PaymentRequest** aRequest, ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCx); + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aTopLevelPrincipal); + *aRequest = nullptr; + + RefPtr<PaymentRequest> request = + PaymentRequest::CreatePaymentRequest(aWindow, aRv); + if (aRv.Failed()) { + return; + } + request->SetOptions(aOptions); + /* + * Set request's |mId| to details.id if details.id exists. + * Otherwise, set |mId| to internal id. + */ + nsAutoString requestId; + if (aDetails.mId.WasPassed() && !aDetails.mId.Value().IsEmpty()) { + requestId = aDetails.mId.Value(); + } else { + request->GetInternalId(requestId); + } + request->SetId(requestId); + + /* + * Set request's |mShippingType| and |mShippingOption| if shipping is + * required. Set request's mShippingOption to last selected option's ID if + * details.shippingOptions exists, otherwise set it as null. + */ + nsAutoString shippingOption; + SetDOMStringToNull(shippingOption); + if (aOptions.mRequestShipping) { + request->ShippingWasRequested(); + request->SetShippingType( + Nullable<PaymentShippingType>(aOptions.mShippingType)); + GetSelectedShippingOption(aDetails, shippingOption); + } + request->SetShippingOption(shippingOption); + + nsAutoString internalId; + request->GetInternalId(internalId); + + nsTArray<IPCPaymentMethodData> methodData; + for (const PaymentMethodData& data : aMethodData) { + IPCPaymentMethodData ipcMethodData; + ConvertMethodData(aCx, data, ipcMethodData, aRv); + if (aRv.Failed()) { + return; + } + methodData.AppendElement(ipcMethodData); + } + + IPCPaymentDetails details; + ConvertDetailsInit(aCx, aDetails, details, aOptions.mRequestShipping, aRv); + if (aRv.Failed()) { + return; + } + + IPCPaymentOptions options; + ConvertOptions(aOptions, options); + + uint64_t topOuterWindowId = + aWindow->GetWindowContext()->TopWindowContext()->OuterWindowId(); + IPCPaymentCreateActionRequest action(topOuterWindowId, internalId, + aTopLevelPrincipal, methodData, details, + options, shippingOption); + + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(request, action, false)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + return; + } + request.forget(aRequest); +} + +void PaymentRequestManager::CanMakePayment(PaymentRequest* aRequest, + ErrorResult& aRv) { + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentCanMakeActionRequest action(requestId); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +void PaymentRequestManager::ShowPayment(PaymentRequest* aRequest, + ErrorResult& aRv) { + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentShowActionRequest action(requestId, aRequest->IsUpdating()); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +void PaymentRequestManager::AbortPayment(PaymentRequest* aRequest, + ErrorResult& aRv) { + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentAbortActionRequest action(requestId); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +void PaymentRequestManager::CompletePayment(PaymentRequest* aRequest, + const PaymentComplete& aComplete, + ErrorResult& aRv, bool aTimedOut) { + nsString completeStatusString(u"unknown"_ns); + if (aTimedOut) { + completeStatusString.AssignLiteral("timeout"); + } else { + completeStatusString.AssignASCII( + PaymentCompleteValues::GetString(aComplete)); + } + + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentCompleteActionRequest action(requestId, completeStatusString); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action, false)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +void PaymentRequestManager::UpdatePayment(JSContext* aCx, + PaymentRequest* aRequest, + const PaymentDetailsUpdate& aDetails, + bool aRequestShipping, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + IPCPaymentDetails details; + ConvertDetailsUpdate(aCx, aDetails, details, aRequestShipping, aRv); + if (aRv.Failed()) { + return; + } + + nsAutoString shippingOption; + SetDOMStringToNull(shippingOption); + if (aRequestShipping) { + GetSelectedShippingOption(aDetails, shippingOption); + aRequest->SetShippingOption(shippingOption); + } + + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentUpdateActionRequest action(requestId, details, shippingOption); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action, false)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +nsresult PaymentRequestManager::ClosePayment(PaymentRequest* aRequest) { + // for the case, the payment request is waiting for response from user. + if (auto entry = mActivePayments.Lookup(aRequest)) { + NotifyRequestDone(aRequest); + } + nsAutoString requestId; + aRequest->GetInternalId(requestId); + IPCPaymentCloseActionRequest action(requestId); + return SendRequestPayment(aRequest, action, false); +} + +void PaymentRequestManager::RetryPayment(JSContext* aCx, + PaymentRequest* aRequest, + const PaymentValidationErrors& aErrors, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aRequest); + + nsAutoString requestId; + aRequest->GetInternalId(requestId); + + nsAutoString error; + if (aErrors.mError.WasPassed()) { + error = aErrors.mError.Value(); + } + + nsAutoString shippingAddressErrors; + if (aErrors.mShippingAddress.WasPassed()) { + if (!aErrors.mShippingAddress.Value().ToJSON(shippingAddressErrors)) { + aRv.ThrowTypeError("The ShippingAddressErrors can not be serialized"); + return; + } + } + + nsAutoString payerErrors; + if (aErrors.mPayer.WasPassed()) { + if (!aErrors.mPayer.Value().ToJSON(payerErrors)) { + aRv.ThrowTypeError("The PayerErrors can not be serialized"); + return; + } + } + + nsAutoString paymentMethodErrors; + if (aErrors.mPaymentMethod.WasPassed()) { + JS::Rooted<JSObject*> object(aCx, aErrors.mPaymentMethod.Value()); + if (NS_WARN_IF(NS_FAILED( + SerializeFromJSObject(aCx, object, paymentMethodErrors)))) { + aRv.ThrowTypeError("The PaymentMethodErrors can not be serialized"); + return; + } + } + IPCPaymentRetryActionRequest action(requestId, error, payerErrors, + paymentMethodErrors, + shippingAddressErrors); + if (NS_WARN_IF(NS_FAILED(SendRequestPayment(aRequest, action)))) { + aRv.ThrowUnknownError("Internal error sending payment request"); + } +} + +nsresult PaymentRequestManager::RespondPayment( + PaymentRequest* aRequest, const IPCPaymentActionResponse& aResponse) { + switch (aResponse.type()) { + case IPCPaymentActionResponse::TIPCPaymentCanMakeActionResponse: { + const IPCPaymentCanMakeActionResponse& response = aResponse; + aRequest->RespondCanMakePayment(response.result()); + NotifyRequestDone(aRequest); + break; + } + case IPCPaymentActionResponse::TIPCPaymentShowActionResponse: { + const IPCPaymentShowActionResponse& response = aResponse; + ErrorResult rejectedReason; + ResponseData responseData; + ConvertResponseData(response.data(), responseData); + switch (response.status()) { + case nsIPaymentActionResponse::PAYMENT_ACCEPTED: { + break; + } + case nsIPaymentActionResponse::PAYMENT_REJECTED: { + rejectedReason.ThrowAbortError("The user rejected the payment"); + break; + } + case nsIPaymentActionResponse::PAYMENT_NOTSUPPORTED: { + rejectedReason.ThrowNotSupportedError("No supported payment method"); + break; + } + default: { + rejectedReason.ThrowUnknownError("Unknown response for the payment"); + break; + } + } + // If PaymentActionResponse is not PAYMENT_ACCEPTED, no need to keep the + // PaymentRequestChild instance. Otherwise, keep PaymentRequestChild for + // merchants call PaymentResponse.complete() + if (rejectedReason.Failed()) { + NotifyRequestDone(aRequest); + } + aRequest->RespondShowPayment(response.methodName(), responseData, + response.payerName(), response.payerEmail(), + response.payerPhone(), + std::move(rejectedReason)); + break; + } + case IPCPaymentActionResponse::TIPCPaymentAbortActionResponse: { + const IPCPaymentAbortActionResponse& response = aResponse; + aRequest->RespondAbortPayment(response.isSucceeded()); + NotifyRequestDone(aRequest); + break; + } + case IPCPaymentActionResponse::TIPCPaymentCompleteActionResponse: { + aRequest->RespondComplete(); + NotifyRequestDone(aRequest); + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +nsresult PaymentRequestManager::ChangeShippingAddress( + PaymentRequest* aRequest, const IPCPaymentAddress& aAddress) { + return aRequest->UpdateShippingAddress( + aAddress.country(), aAddress.addressLine(), aAddress.region(), + aAddress.regionCode(), aAddress.city(), aAddress.dependentLocality(), + aAddress.postalCode(), aAddress.sortingCode(), aAddress.organization(), + aAddress.recipient(), aAddress.phone()); +} + +nsresult PaymentRequestManager::ChangeShippingOption(PaymentRequest* aRequest, + const nsAString& aOption) { + return aRequest->UpdateShippingOption(aOption); +} + +nsresult PaymentRequestManager::ChangePayerDetail( + PaymentRequest* aRequest, const nsAString& aPayerName, + const nsAString& aPayerEmail, const nsAString& aPayerPhone) { + MOZ_ASSERT(aRequest); + RefPtr<PaymentResponse> response = aRequest->GetResponse(); + // ignoring the case call changePayerDetail during show(). + if (!response) { + return NS_OK; + } + return response->UpdatePayerDetail(aPayerName, aPayerEmail, aPayerPhone); +} + +nsresult PaymentRequestManager::ChangePaymentMethod( + PaymentRequest* aRequest, const nsAString& aMethodName, + const IPCMethodChangeDetails& aMethodDetails) { + NS_ENSURE_ARG_POINTER(aRequest); + ChangeDetails methodDetails; + ConvertMethodChangeDetails(aMethodDetails, methodDetails); + return aRequest->UpdatePaymentMethod(aMethodName, methodDetails); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentRequestManager.h b/dom/payments/PaymentRequestManager.h new file mode 100644 index 0000000000..28ed2ac702 --- /dev/null +++ b/dom/payments/PaymentRequestManager.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestManager_h +#define mozilla_dom_PaymentRequestManager_h + +#include "nsISupports.h" +#include "PaymentRequest.h" +#include "mozilla/dom/PaymentRequestBinding.h" +#include "mozilla/dom/PaymentRequestUpdateEventBinding.h" +#include "mozilla/dom/PaymentResponseBinding.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class PaymentRequestChild; +class IPCMethodChangeDetails; +class IPCPaymentAddress; +class IPCPaymentActionResponse; +class IPCPaymentActionRequest; + +/* + * PaymentRequestManager is a singleton used to manage the created + * PaymentRequests. It is also the communication agent to chrome process. + */ +class PaymentRequestManager final { + public: + NS_INLINE_DECL_REFCOUNTING(PaymentRequestManager) + + static already_AddRefed<PaymentRequestManager> GetSingleton(); + + /* + * This method is used to create PaymentRequest object and send corresponding + * data to chrome process for internal payment creation, such that content + * process can ask specific task by sending requestId only. + */ + void CreatePayment(JSContext* aCx, nsPIDOMWindowInner* aWindow, + nsIPrincipal* aTopLevelPrincipal, + const Sequence<PaymentMethodData>& aMethodData, + const PaymentDetailsInit& aDetails, + const PaymentOptions& aOptions, PaymentRequest** aRequest, + ErrorResult& aRv); + + void CanMakePayment(PaymentRequest* aRequest, ErrorResult& aRv); + void ShowPayment(PaymentRequest* aRequest, ErrorResult& aRv); + void AbortPayment(PaymentRequest* aRequest, ErrorResult& aRv); + void CompletePayment(PaymentRequest* aRequest, + const PaymentComplete& aComplete, ErrorResult& aRv, + bool aTimedOut = false); + void UpdatePayment(JSContext* aCx, PaymentRequest* aRequest, + const PaymentDetailsUpdate& aDetails, + bool aRequestShipping, ErrorResult& aRv); + nsresult ClosePayment(PaymentRequest* aRequest); + void RetryPayment(JSContext* aCx, PaymentRequest* aRequest, + const PaymentValidationErrors& aErrors, ErrorResult& aRv); + + nsresult RespondPayment(PaymentRequest* aRequest, + const IPCPaymentActionResponse& aResponse); + nsresult ChangeShippingAddress(PaymentRequest* aRequest, + const IPCPaymentAddress& aAddress); + nsresult ChangeShippingOption(PaymentRequest* aRequest, + const nsAString& aOption); + nsresult ChangePayerDetail(PaymentRequest* aRequest, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone); + nsresult ChangePaymentMethod(PaymentRequest* aRequest, + const nsAString& aMethodName, + const IPCMethodChangeDetails& aMethodDetails); + + bool IsRegionSupported(const nsAString& region) const; + + // Called to ensure that we don't "leak" aRequest if we shut down while it had + // an active request to the parent. + void RequestIPCOver(PaymentRequest* aRequest); + + private: + PaymentRequestManager(); + ~PaymentRequestManager(); + + PaymentRequestChild* GetPaymentChild(PaymentRequest* aRequest); + + nsresult SendRequestPayment(PaymentRequest* aRequest, + const IPCPaymentActionRequest& action, + bool aResponseExpected = true); + + void NotifyRequestDone(PaymentRequest* aRequest); + + // Strong pointer to requests with ongoing IPC messages to the parent. + nsTHashMap<nsRefPtrHashKey<PaymentRequest>, uint32_t> mActivePayments; + + nsTArray<nsString> mSupportedRegions; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/PaymentRequestService.cpp b/dom/payments/PaymentRequestService.cpp new file mode 100644 index 0000000000..3de2cba226 --- /dev/null +++ b/dom/payments/PaymentRequestService.cpp @@ -0,0 +1,606 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BasicCardPayment.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/BasicCardPaymentBinding.h" +#include "mozilla/dom/PaymentRequestParent.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIMutableArray.h" +#include "nsServiceManagerUtils.h" +#include "nsSimpleEnumerator.h" +#include "PaymentRequestService.h" + +namespace mozilla::dom { + +StaticRefPtr<PaymentRequestService> gPaymentService; + +namespace { + +class PaymentRequestEnumerator final : public nsSimpleEnumerator { + public: + NS_DECL_NSISIMPLEENUMERATOR + + PaymentRequestEnumerator() : mIndex(0) {} + + const nsID& DefaultInterface() override { + return NS_GET_IID(nsIPaymentRequest); + } + + private: + ~PaymentRequestEnumerator() override = default; + uint32_t mIndex; +}; + +NS_IMETHODIMP +PaymentRequestEnumerator::HasMoreElements(bool* aReturn) { + NS_ENSURE_ARG_POINTER(aReturn); + *aReturn = false; + if (NS_WARN_IF(!gPaymentService)) { + return NS_ERROR_FAILURE; + } + RefPtr<PaymentRequestService> service = gPaymentService; + *aReturn = mIndex < service->NumPayments(); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestEnumerator::GetNext(nsISupports** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + if (NS_WARN_IF(!gPaymentService)) { + return NS_ERROR_FAILURE; + } + RefPtr<payments::PaymentRequest> rowRequest = + gPaymentService->GetPaymentRequestByIndex(mIndex); + if (!rowRequest) { + return NS_ERROR_FAILURE; + } + mIndex++; + rowRequest.forget(aItem); + return NS_OK; +} + +} // end of anonymous namespace + +/* PaymentRequestService */ + +NS_IMPL_ISUPPORTS(PaymentRequestService, nsIPaymentRequestService) + +already_AddRefed<PaymentRequestService> PaymentRequestService::GetSingleton() { + MOZ_ASSERT(NS_IsMainThread()); + if (!gPaymentService) { + gPaymentService = new PaymentRequestService(); + ClearOnShutdown(&gPaymentService); + } + RefPtr<PaymentRequestService> service = gPaymentService; + return service.forget(); +} + +uint32_t PaymentRequestService::NumPayments() const { + return mRequestQueue.Length(); +} + +already_AddRefed<payments::PaymentRequest> +PaymentRequestService::GetPaymentRequestByIndex(const uint32_t aIndex) { + if (aIndex >= mRequestQueue.Length()) { + return nullptr; + } + RefPtr<payments::PaymentRequest> request = mRequestQueue[aIndex]; + MOZ_ASSERT(request); + return request.forget(); +} + +NS_IMETHODIMP +PaymentRequestService::GetPaymentRequestById(const nsAString& aRequestId, + nsIPaymentRequest** aRequest) { + NS_ENSURE_ARG_POINTER(aRequest); + *aRequest = nullptr; + RefPtr<payments::PaymentRequest> rowRequest; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(rowRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rowRequest.forget(aRequest); + return NS_OK; +} + +nsresult PaymentRequestService::GetPaymentRequestById( + const nsAString& aRequestId, payments::PaymentRequest** aRequest) { + NS_ENSURE_ARG_POINTER(aRequest); + *aRequest = nullptr; + uint32_t numRequests = mRequestQueue.Length(); + for (uint32_t index = 0; index < numRequests; ++index) { + RefPtr<payments::PaymentRequest> request = mRequestQueue[index]; + MOZ_ASSERT(request); + nsAutoString requestId; + nsresult rv = request->GetRequestId(requestId); + NS_ENSURE_SUCCESS(rv, rv); + if (requestId == aRequestId) { + request.forget(aRequest); + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::Enumerate(nsISimpleEnumerator** aEnumerator) { + NS_ENSURE_ARG_POINTER(aEnumerator); + nsCOMPtr<nsISimpleEnumerator> enumerator = new PaymentRequestEnumerator(); + enumerator.forget(aEnumerator); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::Cleanup() { + mRequestQueue.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::SetTestingUIService(nsIPaymentUIService* aUIService) { + // aUIService can be nullptr + mTestingUIService = aUIService; + return NS_OK; +} + +nsresult PaymentRequestService::LaunchUIAction(const nsAString& aRequestId, + uint32_t aActionType) { + nsCOMPtr<nsIPaymentUIService> uiService; + nsresult rv; + if (mTestingUIService) { + uiService = mTestingUIService; + } else { + uiService = do_GetService(NS_PAYMENT_UI_SERVICE_CONTRACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + switch (aActionType) { + case IPCPaymentActionRequest::TIPCPaymentShowActionRequest: { + rv = uiService->ShowPayment(aRequestId); + break; + } + case IPCPaymentActionRequest::TIPCPaymentAbortActionRequest: { + rv = uiService->AbortPayment(aRequestId); + break; + } + case IPCPaymentActionRequest::TIPCPaymentCompleteActionRequest: { + rv = uiService->CompletePayment(aRequestId); + break; + } + case IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest: { + rv = uiService->UpdatePayment(aRequestId); + break; + } + case IPCPaymentActionRequest::TIPCPaymentCloseActionRequest: { + rv = uiService->ClosePayment(aRequestId); + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +nsresult PaymentRequestService::RequestPayment( + const nsAString& aRequestId, const IPCPaymentActionRequest& aAction, + PaymentRequestParent* aIPC) { + NS_ENSURE_ARG_POINTER(aIPC); + + nsresult rv = NS_OK; + uint32_t type = aAction.type(); + + RefPtr<payments::PaymentRequest> request; + if (type != IPCPaymentActionRequest::TIPCPaymentCreateActionRequest) { + rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!request && + type != IPCPaymentActionRequest::TIPCPaymentCloseActionRequest) { + return NS_ERROR_FAILURE; + } + if (request) { + request->SetIPC(aIPC); + } + } + + switch (type) { + case IPCPaymentActionRequest::TIPCPaymentCreateActionRequest: { + MOZ_ASSERT(!request); + const IPCPaymentCreateActionRequest& action = aAction; + nsCOMPtr<nsIMutableArray> methodData = + do_CreateInstance(NS_ARRAY_CONTRACTID); + MOZ_ASSERT(methodData); + for (IPCPaymentMethodData data : action.methodData()) { + nsCOMPtr<nsIPaymentMethodData> method; + rv = payments::PaymentMethodData::Create(data, getter_AddRefs(method)); + NS_ENSURE_SUCCESS(rv, rv); + rv = methodData->AppendElement(method); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCOMPtr<nsIPaymentDetails> details; + rv = payments::PaymentDetails::Create(action.details(), + getter_AddRefs(details)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPaymentOptions> options; + rv = payments::PaymentOptions::Create(action.options(), + getter_AddRefs(options)); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<payments::PaymentRequest> request = new payments::PaymentRequest( + action.topOuterWindowId(), aRequestId, action.topLevelPrincipal(), + methodData, details, options, action.shippingOption()); + + if (!mRequestQueue.AppendElement(request, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentCanMakeActionRequest: { + nsCOMPtr<nsIPaymentCanMakeActionResponse> canMakeResponse = + do_CreateInstance(NS_PAYMENT_CANMAKE_ACTION_RESPONSE_CONTRACT_ID); + MOZ_ASSERT(canMakeResponse); + rv = canMakeResponse->Init(aRequestId, CanMakePayment(aRequestId)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = RespondPayment(canMakeResponse.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentShowActionRequest: { + const IPCPaymentShowActionRequest& action = aAction; + rv = ShowPayment(aRequestId, action.isUpdating()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentAbortActionRequest: { + MOZ_ASSERT(request); + request->SetState(payments::PaymentRequest::eInteractive); + rv = LaunchUIAction(aRequestId, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentCompleteActionRequest: { + MOZ_ASSERT(request); + const IPCPaymentCompleteActionRequest& action = aAction; + request->SetCompleteStatus(action.completeStatus()); + rv = LaunchUIAction(aRequestId, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest: { + const IPCPaymentUpdateActionRequest& action = aAction; + nsCOMPtr<nsIPaymentDetails> details; + rv = payments::PaymentDetails::Create(action.details(), + getter_AddRefs(details)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(request); + rv = request->UpdatePaymentDetails(details, action.shippingOption()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsAutoString completeStatus; + rv = request->GetCompleteStatus(completeStatus); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (completeStatus.Equals(u"initial"_ns)) { + request->SetCompleteStatus(u""_ns); + } + MOZ_ASSERT(mShowingRequest && mShowingRequest == request); + rv = LaunchUIAction(aRequestId, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + break; + } + case IPCPaymentActionRequest::TIPCPaymentCloseActionRequest: { + rv = LaunchUIAction(aRequestId, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (mShowingRequest == request) { + mShowingRequest = nullptr; + } + mRequestQueue.RemoveElement(request); + break; + } + case IPCPaymentActionRequest::TIPCPaymentRetryActionRequest: { + const IPCPaymentRetryActionRequest& action = aAction; + MOZ_ASSERT(request); + request->UpdateErrors(action.error(), action.payerErrors(), + action.paymentMethodErrors(), + action.shippingAddressErrors()); + request->SetState(payments::PaymentRequest::eInteractive); + MOZ_ASSERT(mShowingRequest == request); + rv = LaunchUIAction( + aRequestId, IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest); + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::RespondPayment(nsIPaymentActionResponse* aResponse) { + NS_ENSURE_ARG_POINTER(aResponse); + nsAutoString requestId; + nsresult rv = aResponse->GetRequestId(requestId); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<payments::PaymentRequest> request; + rv = GetPaymentRequestById(requestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!request) { + return NS_ERROR_FAILURE; + } + uint32_t type; + rv = aResponse->GetType(&type); + NS_ENSURE_SUCCESS(rv, rv); + + // PaymentRequest can only be responded when + // 1. the state is eInteractive + // 2. the state is eClosed and response type is COMPLETE_ACTION + // 3. the state is eCreated and response type is CANMAKE_ACTION + payments::PaymentRequest::eState state = request->GetState(); + bool canBeResponded = (state == payments::PaymentRequest::eInteractive) || + (state == payments::PaymentRequest::eClosed && + type == nsIPaymentActionResponse::COMPLETE_ACTION) || + (state == payments::PaymentRequest::eCreated && + type == nsIPaymentActionResponse::CANMAKE_ACTION); + if (!canBeResponded) { + return NS_ERROR_FAILURE; + } + + if (!request->GetIPC()) { + return NS_ERROR_FAILURE; + } + rv = request->GetIPC()->RespondPayment(aResponse); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Remove PaymentRequest from mRequestQueue while receive succeeded abort + // response or complete response + switch (type) { + case nsIPaymentActionResponse::ABORT_ACTION: { + nsCOMPtr<nsIPaymentAbortActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + bool isSucceeded; + rv = response->IsSucceeded(&isSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + mShowingRequest = nullptr; + if (isSucceeded) { + mRequestQueue.RemoveElement(request); + request->SetState(payments::PaymentRequest::eClosed); + } + break; + } + case nsIPaymentActionResponse::SHOW_ACTION: { + request->SetState(payments::PaymentRequest::eClosed); + nsCOMPtr<nsIPaymentShowActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + uint32_t acceptStatus; + rv = response->GetAcceptStatus(&acceptStatus); + NS_ENSURE_SUCCESS(rv, rv); + if (acceptStatus != nsIPaymentActionResponse::PAYMENT_ACCEPTED) { + // Check if rejecting the showing PaymentRequest. + // If yes, set mShowingRequest as nullptr. + if (mShowingRequest == request) { + mShowingRequest = nullptr; + } + mRequestQueue.RemoveElement(request); + } + break; + } + case nsIPaymentActionResponse::COMPLETE_ACTION: { + mShowingRequest = nullptr; + mRequestQueue.RemoveElement(request); + break; + } + default: { + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::ChangeShippingAddress(const nsAString& aRequestId, + nsIPaymentAddress* aAddress) { + RefPtr<payments::PaymentRequest> request; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!request) { + return NS_ERROR_FAILURE; + } + if (request->GetState() != payments::PaymentRequest::eInteractive) { + return NS_ERROR_FAILURE; + } + if (!request->GetIPC()) { + return NS_ERROR_FAILURE; + } + rv = request->GetIPC()->ChangeShippingAddress(aRequestId, aAddress); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::ChangeShippingOption(const nsAString& aRequestId, + const nsAString& aOption) { + RefPtr<payments::PaymentRequest> request; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!request) { + return NS_ERROR_FAILURE; + } + if (request->GetState() != payments::PaymentRequest::eInteractive) { + return NS_ERROR_FAILURE; + } + if (!request->GetIPC()) { + return NS_ERROR_FAILURE; + } + rv = request->GetIPC()->ChangeShippingOption(aRequestId, aOption); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::ChangePayerDetail(const nsAString& aRequestId, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone) { + RefPtr<payments::PaymentRequest> request; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(request); + if (!request->GetIPC()) { + return NS_ERROR_FAILURE; + } + rv = request->GetIPC()->ChangePayerDetail(aRequestId, aPayerName, aPayerEmail, + aPayerPhone); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +PaymentRequestService::ChangePaymentMethod( + const nsAString& aRequestId, const nsAString& aMethodName, + nsIMethodChangeDetails* aMethodDetails) { + RefPtr<payments::PaymentRequest> request; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!request) { + return NS_ERROR_FAILURE; + } + if (request->GetState() != payments::PaymentRequest::eInteractive) { + return NS_ERROR_FAILURE; + } + if (!request->GetIPC()) { + return NS_ERROR_FAILURE; + } + rv = request->GetIPC()->ChangePaymentMethod(aRequestId, aMethodName, + aMethodDetails); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +bool PaymentRequestService::CanMakePayment(const nsAString& aRequestId) { + /* + * TODO: Check third party payment app support by traversing all + * registered third party payment apps. + */ + return IsBasicCardPayment(aRequestId); +} + +nsresult PaymentRequestService::ShowPayment(const nsAString& aRequestId, + bool aIsUpdating) { + nsresult rv; + RefPtr<payments::PaymentRequest> request; + rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(request); + request->SetState(payments::PaymentRequest::eInteractive); + if (aIsUpdating) { + request->SetCompleteStatus(u"initial"_ns); + } + + if (mShowingRequest || !CanMakePayment(aRequestId)) { + uint32_t responseStatus; + if (mShowingRequest) { + responseStatus = nsIPaymentActionResponse::PAYMENT_REJECTED; + } else { + responseStatus = nsIPaymentActionResponse::PAYMENT_NOTSUPPORTED; + } + nsCOMPtr<nsIPaymentShowActionResponse> showResponse = + do_CreateInstance(NS_PAYMENT_SHOW_ACTION_RESPONSE_CONTRACT_ID); + MOZ_ASSERT(showResponse); + rv = showResponse->Init(aRequestId, responseStatus, u""_ns, nullptr, u""_ns, + u""_ns, u""_ns); + rv = RespondPayment(showResponse.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + mShowingRequest = request; + rv = LaunchUIAction(aRequestId, + IPCPaymentActionRequest::TIPCPaymentShowActionRequest); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + return NS_OK; +} + +bool PaymentRequestService::IsBasicCardPayment(const nsAString& aRequestId) { + RefPtr<payments::PaymentRequest> request; + nsresult rv = GetPaymentRequestById(aRequestId, getter_AddRefs(request)); + NS_ENSURE_SUCCESS(rv, false); + nsCOMPtr<nsIArray> methods; + rv = request->GetPaymentMethods(getter_AddRefs(methods)); + NS_ENSURE_SUCCESS(rv, false); + uint32_t length; + rv = methods->GetLength(&length); + NS_ENSURE_SUCCESS(rv, false); + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + for (uint32_t index = 0; index < length; ++index) { + nsCOMPtr<nsIPaymentMethodData> method = do_QueryElementAt(methods, index); + MOZ_ASSERT(method); + nsAutoString supportedMethods; + rv = method->GetSupportedMethods(supportedMethods); + NS_ENSURE_SUCCESS(rv, false); + if (service->IsBasicCardPayment(supportedMethods)) { + return true; + } + } + return false; +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentRequestService.h b/dom/payments/PaymentRequestService.h new file mode 100644 index 0000000000..5ff67e7a5f --- /dev/null +++ b/dom/payments/PaymentRequestService.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestService_h +#define mozilla_dom_PaymentRequestService_h + +#include "nsInterfaceHashtable.h" +#include "nsIPaymentRequestService.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "PaymentRequestData.h" + +namespace mozilla::dom { + +// The implmentation of nsIPaymentRequestService + +class PaymentRequestService final : public nsIPaymentRequestService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPAYMENTREQUESTSERVICE + + PaymentRequestService() = default; + + static already_AddRefed<PaymentRequestService> GetSingleton(); + + already_AddRefed<payments::PaymentRequest> GetPaymentRequestByIndex( + const uint32_t index); + + uint32_t NumPayments() const; + + nsresult RequestPayment(const nsAString& aRequestId, + const IPCPaymentActionRequest& aAction, + PaymentRequestParent* aCallback); + + private: + ~PaymentRequestService() = default; + + nsresult GetPaymentRequestById(const nsAString& aRequestId, + payments::PaymentRequest** aRequest); + + nsresult LaunchUIAction(const nsAString& aRequestId, uint32_t aActionType); + + bool CanMakePayment(const nsAString& aRequestId); + + nsresult ShowPayment(const nsAString& aRequestId, bool aIsUpdating); + + bool IsBasicCardPayment(const nsAString& aRequestId); + + FallibleTArray<RefPtr<payments::PaymentRequest>> mRequestQueue; + + nsCOMPtr<nsIPaymentUIService> mTestingUIService; + + RefPtr<payments::PaymentRequest> mShowingRequest; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/PaymentRequestUpdateEvent.cpp b/dom/payments/PaymentRequestUpdateEvent.cpp new file mode 100644 index 0000000000..12c4a8c287 --- /dev/null +++ b/dom/payments/PaymentRequestUpdateEvent.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PaymentRequestUpdateEvent.h" +#include "mozilla/dom/PaymentRequest.h" +#include "mozilla/dom/RootedDictionary.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PaymentRequestUpdateEvent, Event, mRequest) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentRequestUpdateEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentRequestUpdateEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(PaymentRequestUpdateEvent, Event) +NS_IMPL_RELEASE_INHERITED(PaymentRequestUpdateEvent, Event) + +already_AddRefed<PaymentRequestUpdateEvent> +PaymentRequestUpdateEvent::Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict) { + RefPtr<PaymentRequestUpdateEvent> e = new PaymentRequestUpdateEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +already_AddRefed<PaymentRequestUpdateEvent> +PaymentRequestUpdateEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict) { + nsCOMPtr<mozilla::dom::EventTarget> owner = + do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aEventInitDict); +} + +PaymentRequestUpdateEvent::PaymentRequestUpdateEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr), + mWaitForUpdate(false), + mRequest(nullptr) { + MOZ_ASSERT(aOwner); +} + +void PaymentRequestUpdateEvent::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(mRequest); + if (!mRequest->InFullyActiveDocument()) { + return; + } + + if (NS_WARN_IF(!aValue.isObject()) || !mWaitForUpdate) { + return; + } + + ErrorResult rv; + // Converting value to a PaymentDetailsUpdate dictionary + RootedDictionary<PaymentDetailsUpdate> details(aCx); + if (!details.Init(aCx, aValue)) { + rv.StealExceptionFromJSContext(aCx); + mRequest->AbortUpdate(rv); + return; + } + + // Validate and canonicalize the details + // requestShipping must be true here. PaymentRequestUpdateEvent is only + // dispatched when shippingAddress/shippingOption is changed, and it also + // means Options.RequestShipping must be true while creating the corresponding + // PaymentRequest. + mRequest->IsValidDetailsUpdate(details, true /*aRequestShipping*/, rv); + if (rv.Failed()) { + mRequest->AbortUpdate(rv); + return; + } + + // Update the PaymentRequest with the new details + mRequest->UpdatePayment(aCx, details, rv); + if (rv.Failed()) { + mRequest->AbortUpdate(rv); + return; + } + + mWaitForUpdate = false; + mRequest->SetUpdating(false); +} + +void PaymentRequestUpdateEvent::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(mRequest); + if (!mRequest->InFullyActiveDocument()) { + return; + } + + ErrorResult rejectReason; + rejectReason.ThrowAbortError( + "Details promise for PaymentRequestUpdateEvent.updateWith() is rejected " + "by merchant"); + mRequest->AbortUpdate(rejectReason); + mWaitForUpdate = false; + mRequest->SetUpdating(false); +} + +void PaymentRequestUpdateEvent::UpdateWith(Promise& aPromise, + ErrorResult& aRv) { + if (!IsTrusted()) { + aRv.ThrowInvalidStateError("Called on an untrusted event"); + return; + } + + MOZ_ASSERT(mRequest); + if (!mRequest->InFullyActiveDocument()) { + return; + } + + if (mWaitForUpdate || !mRequest->ReadyForUpdate()) { + aRv.ThrowInvalidStateError( + "The PaymentRequestUpdateEvent is waiting for update"); + return; + } + + if (!mRequest->ReadyForUpdate()) { + aRv.ThrowInvalidStateError( + "The PaymentRequest state is not eInteractive or is the PaymentRequest " + "is updating"); + return; + } + + aPromise.AppendNativeHandler(this); + + StopPropagation(); + StopImmediatePropagation(); + mWaitForUpdate = true; + mRequest->SetUpdating(true); +} + +void PaymentRequestUpdateEvent::SetRequest(PaymentRequest* aRequest) { + MOZ_ASSERT(IsTrusted()); + MOZ_ASSERT(!mRequest); + MOZ_ASSERT(aRequest); + + mRequest = aRequest; +} + +PaymentRequestUpdateEvent::~PaymentRequestUpdateEvent() = default; + +JSObject* PaymentRequestUpdateEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return PaymentRequestUpdateEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentRequestUpdateEvent.h b/dom/payments/PaymentRequestUpdateEvent.h new file mode 100644 index 0000000000..5ed0dd295a --- /dev/null +++ b/dom/payments/PaymentRequestUpdateEvent.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestUpdateEvent_h +#define mozilla_dom_PaymentRequestUpdateEvent_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/PaymentRequestUpdateEventBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class Promise; +class PaymentRequest; +class PaymentRequestUpdateEvent : public Event, public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + PaymentRequestUpdateEvent, Event) + + explicit PaymentRequestUpdateEvent(EventTarget* aOwner); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + static already_AddRefed<PaymentRequestUpdateEvent> Constructor( + EventTarget* aOwner, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict); + + // Called by WebIDL constructor + static already_AddRefed<PaymentRequestUpdateEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PaymentRequestUpdateEventInit& aEventInitDict); + + void UpdateWith(Promise& aPromise, ErrorResult& aRv); + + void SetRequest(PaymentRequest* aRequest); + + protected: + ~PaymentRequestUpdateEvent(); + // Indicating whether an updateWith()-initiated update is currently in + // progress. + bool mWaitForUpdate; + RefPtr<PaymentRequest> mRequest; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PaymentRequestUpdateEvent_h diff --git a/dom/payments/PaymentRequestUtils.cpp b/dom/payments/PaymentRequestUtils.cpp new file mode 100644 index 0000000000..8a8626c328 --- /dev/null +++ b/dom/payments/PaymentRequestUtils.cpp @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "js/JSON.h" +#include "nsContentUtils.h" +#include "nsArrayUtils.h" +#include "nsTString.h" +#include "PaymentRequestUtils.h" + +namespace mozilla::dom { + +nsresult SerializeFromJSObject(JSContext* aCx, JS::Handle<JSObject*> aObject, + nsAString& aSerializedObject) { + MOZ_ASSERT(aCx); + JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*aObject)); + return SerializeFromJSVal(aCx, value, aSerializedObject); +} + +nsresult SerializeFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aValue, + nsAString& aSerializedValue) { + aSerializedValue.Truncate(); + NS_ENSURE_TRUE(nsContentUtils::StringifyJSON(aCx, aValue, aSerializedValue, + UndefinedIsNullStringLiteral), + NS_ERROR_XPC_BAD_CONVERT_JS); + NS_ENSURE_TRUE(!aSerializedValue.IsEmpty(), NS_ERROR_FAILURE); + return NS_OK; +} + +nsresult DeserializeToJSObject(const nsAString& aSerializedObject, + JSContext* aCx, + JS::MutableHandle<JSObject*> aObject) { + MOZ_ASSERT(aCx); + JS::Rooted<JS::Value> value(aCx); + nsresult rv = DeserializeToJSValue(aSerializedObject, aCx, &value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (value.isObject()) { + aObject.set(&value.toObject()); + } else { + aObject.set(nullptr); + } + return NS_OK; +} + +nsresult DeserializeToJSValue(const nsAString& aSerializedObject, + JSContext* aCx, + JS::MutableHandle<JS::Value> aValue) { + MOZ_ASSERT(aCx); + if (!JS_ParseJSON(aCx, aSerializedObject.BeginReading(), + aSerializedObject.Length(), aValue)) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentRequestUtils.h b/dom/payments/PaymentRequestUtils.h new file mode 100644 index 0000000000..983faaab93 --- /dev/null +++ b/dom/payments/PaymentRequestUtils.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestUtils_h +#define mozilla_dom_PaymentRequestUtils_h + +#include "js/TypeDecls.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +nsresult SerializeFromJSObject(JSContext* aCx, JS::Handle<JSObject*> aObject, + nsAString& aSerializedObject); + +nsresult SerializeFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aValue, + nsAString& aSerializedValue); + +nsresult DeserializeToJSObject(const nsAString& aSerializedObject, + JSContext* aCx, + JS::MutableHandle<JSObject*> aObject); + +nsresult DeserializeToJSValue(const nsAString& aSerializedObject, + JSContext* aCx, + JS::MutableHandle<JS::Value> aValue); + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/PaymentResponse.cpp b/dom/payments/PaymentResponse.cpp new file mode 100644 index 0000000000..1f107b0747 --- /dev/null +++ b/dom/payments/PaymentResponse.cpp @@ -0,0 +1,458 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/PaymentResponse.h" +#include "mozilla/dom/BasicCardPaymentBinding.h" +#include "mozilla/dom/PaymentRequestUpdateEvent.h" +#include "BasicCardPayment.h" +#include "PaymentAddress.h" +#include "PaymentRequest.h" +#include "PaymentRequestManager.h" +#include "PaymentRequestUtils.h" +#include "mozilla/EventStateManager.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PaymentResponse) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PaymentResponse, + DOMEventTargetHelper) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PaymentResponse, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTimer) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PaymentResponse, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mShippingAddress) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTimer) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaymentResponse) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(PaymentResponse, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(PaymentResponse, DOMEventTargetHelper) + +PaymentResponse::PaymentResponse( + nsPIDOMWindowInner* aWindow, PaymentRequest* aRequest, + const nsAString& aRequestId, const nsAString& aMethodName, + const nsAString& aShippingOption, PaymentAddress* aShippingAddress, + const ResponseData& aDetails, const nsAString& aPayerName, + const nsAString& aPayerEmail, const nsAString& aPayerPhone) + : DOMEventTargetHelper(aWindow), + mCompleteCalled(false), + mRequest(aRequest), + mRequestId(aRequestId), + mMethodName(aMethodName), + mDetails(aDetails), + mShippingOption(aShippingOption), + mPayerName(aPayerName), + mPayerEmail(aPayerEmail), + mPayerPhone(aPayerPhone), + mShippingAddress(aShippingAddress) { + // TODO: from https://github.com/w3c/browser-payment-api/issues/480 + // Add payerGivenName + payerFamilyName to PaymentAddress + NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, + StaticPrefs::dom_payments_response_timeout(), + nsITimer::TYPE_ONE_SHOT, + aWindow->EventTargetFor(TaskCategory::Other)); +} + +PaymentResponse::~PaymentResponse() = default; + +JSObject* PaymentResponse::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaymentResponse_Binding::Wrap(aCx, this, aGivenProto); +} + +void PaymentResponse::GetRequestId(nsString& aRetVal) const { + aRetVal = mRequestId; +} + +void PaymentResponse::GetMethodName(nsString& aRetVal) const { + aRetVal = mMethodName; +} + +void PaymentResponse::GetDetails(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetVal) const { + switch (mDetails.type()) { + case ResponseData::GeneralResponse: { + const GeneralData& rawData = mDetails.generalData(); + DeserializeToJSObject(rawData.data, aCx, aRetVal); + break; + } + case ResponseData::BasicCardResponse: { + const BasicCardData& rawData = mDetails.basicCardData(); + BasicCardResponse basicCardResponse; + if (!rawData.cardholderName.IsEmpty()) { + basicCardResponse.mCardholderName = rawData.cardholderName; + } + basicCardResponse.mCardNumber = rawData.cardNumber; + if (!rawData.expiryMonth.IsEmpty()) { + basicCardResponse.mExpiryMonth = rawData.expiryMonth; + } + if (!rawData.expiryYear.IsEmpty()) { + basicCardResponse.mExpiryYear = rawData.expiryYear; + } + if (!rawData.cardSecurityCode.IsEmpty()) { + basicCardResponse.mCardSecurityCode = rawData.cardSecurityCode; + } + if (!rawData.billingAddress.country.IsEmpty() || + !rawData.billingAddress.addressLine.IsEmpty() || + !rawData.billingAddress.region.IsEmpty() || + !rawData.billingAddress.regionCode.IsEmpty() || + !rawData.billingAddress.city.IsEmpty() || + !rawData.billingAddress.dependentLocality.IsEmpty() || + !rawData.billingAddress.postalCode.IsEmpty() || + !rawData.billingAddress.sortingCode.IsEmpty() || + !rawData.billingAddress.organization.IsEmpty() || + !rawData.billingAddress.recipient.IsEmpty() || + !rawData.billingAddress.phone.IsEmpty()) { + basicCardResponse.mBillingAddress = new PaymentAddress( + GetOwner(), rawData.billingAddress.country, + rawData.billingAddress.addressLine, rawData.billingAddress.region, + rawData.billingAddress.regionCode, rawData.billingAddress.city, + rawData.billingAddress.dependentLocality, + rawData.billingAddress.postalCode, + rawData.billingAddress.sortingCode, + rawData.billingAddress.organization, + rawData.billingAddress.recipient, rawData.billingAddress.phone); + } + MOZ_ASSERT(aCx); + JS::Rooted<JS::Value> value(aCx); + if (NS_WARN_IF(!basicCardResponse.ToObjectInternal(aCx, &value))) { + return; + } + aRetVal.set(&value.toObject()); + break; + } + default: { + MOZ_ASSERT(false); + break; + } + } +} + +void PaymentResponse::GetShippingOption(nsString& aRetVal) const { + aRetVal = mShippingOption; +} + +void PaymentResponse::GetPayerName(nsString& aRetVal) const { + aRetVal = mPayerName; +} + +void PaymentResponse::GetPayerEmail(nsString& aRetVal) const { + aRetVal = mPayerEmail; +} + +void PaymentResponse::GetPayerPhone(nsString& aRetVal) const { + aRetVal = mPayerPhone; +} + +// TODO: +// Return a raw pointer here to avoid refcounting, but make sure it's safe +// (the object should be kept alive by the callee). +already_AddRefed<PaymentAddress> PaymentResponse::GetShippingAddress() const { + RefPtr<PaymentAddress> address = mShippingAddress; + return address.forget(); +} + +already_AddRefed<Promise> PaymentResponse::Complete(PaymentComplete result, + ErrorResult& aRv) { + MOZ_ASSERT(mRequest); + if (!mRequest->InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + if (mCompleteCalled) { + aRv.ThrowInvalidStateError( + "PaymentResponse.complete() has already been called"); + return nullptr; + } + + mCompleteCalled = true; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + manager->CompletePayment(mRequest, result, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (NS_WARN_IF(!GetOwner())) { + aRv.ThrowAbortError("Global object should exist"); + return nullptr; + } + + nsIGlobalObject* global = GetOwner()->AsGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mPromise = promise; + return promise.forget(); +} + +void PaymentResponse::RespondComplete() { + // mPromise may be null when timing out + if (mPromise) { + mPromise->MaybeResolve(JS::UndefinedHandleValue); + mPromise = nullptr; + } +} + +already_AddRefed<Promise> PaymentResponse::Retry( + JSContext* aCx, const PaymentValidationErrors& aErrors, ErrorResult& aRv) { + MOZ_ASSERT(mRequest); + if (!mRequest->InFullyActiveDocument()) { + aRv.ThrowAbortError("The owner document is not fully active"); + return nullptr; + } + + nsIGlobalObject* global = GetOwner()->AsGlobal(); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + if (mCompleteCalled || mRetryPromise) { + aRv.ThrowInvalidStateError( + "PaymentResponse.complete() has already been called"); + return nullptr; + } + + if (mRetryPromise) { + aRv.ThrowInvalidStateError("Is retrying the PaymentRequest"); + return nullptr; + } + + ValidatePaymentValidationErrors(aErrors, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Depending on the PMI, try to do IDL type conversion + // (e.g., basic-card expects at BasicCardErrors dictionary) + ConvertPaymentMethodErrors(aCx, aErrors, aRv); + if (aRv.Failed()) { + return nullptr; + } + + MOZ_ASSERT(mRequest); + mRequest->RetryPayment(aCx, aErrors, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mRetryPromise = promise; + return promise.forget(); +} + +void PaymentResponse::RespondRetry(const nsAString& aMethodName, + const nsAString& aShippingOption, + PaymentAddress* aShippingAddress, + const ResponseData& aDetails, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone) { + // mRetryPromise could be nulled when document activity is changed. + if (!mRetryPromise) { + return; + } + mMethodName = aMethodName; + mShippingOption = aShippingOption; + mShippingAddress = aShippingAddress; + mDetails = aDetails; + mPayerName = aPayerName; + mPayerEmail = aPayerEmail; + mPayerPhone = aPayerPhone; + + if (NS_WARN_IF(!GetOwner())) { + return; + } + + NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, + StaticPrefs::dom_payments_response_timeout(), + nsITimer::TYPE_ONE_SHOT, + GetOwner()->EventTargetFor(TaskCategory::Other)); + MOZ_ASSERT(mRetryPromise); + mRetryPromise->MaybeResolve(JS::UndefinedHandleValue); + mRetryPromise = nullptr; +} + +void PaymentResponse::RejectRetry(ErrorResult&& aRejectReason) { + MOZ_ASSERT(mRetryPromise); + mRetryPromise->MaybeReject(std::move(aRejectReason)); + mRetryPromise = nullptr; +} + +void PaymentResponse::ConvertPaymentMethodErrors( + JSContext* aCx, const PaymentValidationErrors& aErrors, + ErrorResult& aRv) const { + MOZ_ASSERT(aCx); + if (!aErrors.mPaymentMethod.WasPassed()) { + return; + } + RefPtr<BasicCardService> service = BasicCardService::GetService(); + MOZ_ASSERT(service); + if (service->IsBasicCardPayment(mMethodName)) { + MOZ_ASSERT(aErrors.mPaymentMethod.Value(), + "The IDL says this is not nullable!"); + service->CheckForValidBasicCardErrors(aCx, aErrors.mPaymentMethod.Value(), + aRv); + } +} + +void PaymentResponse::ValidatePaymentValidationErrors( + const PaymentValidationErrors& aErrors, ErrorResult& aRv) { + // Should not be empty errors + // check PaymentValidationErrors.error + if (aErrors.mError.WasPassed() && !aErrors.mError.Value().IsEmpty()) { + return; + } + // check PaymentValidationErrors.payer + if (aErrors.mPayer.WasPassed()) { + PayerErrors payerErrors(aErrors.mPayer.Value()); + if (payerErrors.mName.WasPassed() && !payerErrors.mName.Value().IsEmpty()) { + return; + } + if (payerErrors.mEmail.WasPassed() && + !payerErrors.mEmail.Value().IsEmpty()) { + return; + } + if (payerErrors.mPhone.WasPassed() && + !payerErrors.mPhone.Value().IsEmpty()) { + return; + } + } + // check PaymentValidationErrors.paymentMethod + if (aErrors.mPaymentMethod.WasPassed()) { + return; + } + // check PaymentValidationErrors.shippingAddress + if (aErrors.mShippingAddress.WasPassed()) { + AddressErrors addErrors(aErrors.mShippingAddress.Value()); + if (addErrors.mAddressLine.WasPassed() && + !addErrors.mAddressLine.Value().IsEmpty()) { + return; + } + if (addErrors.mCity.WasPassed() && !addErrors.mCity.Value().IsEmpty()) { + return; + } + if (addErrors.mCountry.WasPassed() && + !addErrors.mCountry.Value().IsEmpty()) { + return; + } + if (addErrors.mDependentLocality.WasPassed() && + !addErrors.mDependentLocality.Value().IsEmpty()) { + return; + } + if (addErrors.mOrganization.WasPassed() && + !addErrors.mOrganization.Value().IsEmpty()) { + return; + } + if (addErrors.mPhone.WasPassed() && !addErrors.mPhone.Value().IsEmpty()) { + return; + } + if (addErrors.mPostalCode.WasPassed() && + !addErrors.mPostalCode.Value().IsEmpty()) { + return; + } + if (addErrors.mRecipient.WasPassed() && + !addErrors.mRecipient.Value().IsEmpty()) { + return; + } + if (addErrors.mRegion.WasPassed() && !addErrors.mRegion.Value().IsEmpty()) { + return; + } + if (addErrors.mRegionCode.WasPassed() && + !addErrors.mRegionCode.Value().IsEmpty()) { + return; + } + if (addErrors.mSortingCode.WasPassed() && + !addErrors.mSortingCode.Value().IsEmpty()) { + return; + } + } + aRv.ThrowAbortError("PaymentValidationErrors can not be an empty error"); +} + +NS_IMETHODIMP +PaymentResponse::Notify(nsITimer* timer) { + mTimer = nullptr; + + if (!mRequest->InFullyActiveDocument()) { + return NS_OK; + } + + if (mCompleteCalled) { + return NS_OK; + } + + mCompleteCalled = true; + + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + if (NS_WARN_IF(!manager)) { + return NS_ERROR_FAILURE; + } + manager->CompletePayment(mRequest, PaymentComplete::Unknown, IgnoreErrors(), + true); + return NS_OK; +} + +nsresult PaymentResponse::UpdatePayerDetail(const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone) { + MOZ_ASSERT(mRequest->ReadyForUpdate()); + PaymentOptions options; + mRequest->GetOptions(options); + if (options.mRequestPayerName) { + mPayerName = aPayerName; + } + if (options.mRequestPayerEmail) { + mPayerEmail = aPayerEmail; + } + if (options.mRequestPayerPhone) { + mPayerPhone = aPayerPhone; + } + return DispatchUpdateEvent(u"payerdetailchange"_ns); +} + +nsresult PaymentResponse::DispatchUpdateEvent(const nsAString& aType) { + PaymentRequestUpdateEventInit init; + RefPtr<PaymentRequestUpdateEvent> event = + PaymentRequestUpdateEvent::Constructor(this, aType, init); + event->SetTrusted(true); + event->SetRequest(mRequest); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +} // namespace mozilla::dom diff --git a/dom/payments/PaymentResponse.h b/dom/payments/PaymentResponse.h new file mode 100644 index 0000000000..aeffe4fac4 --- /dev/null +++ b/dom/payments/PaymentResponse.h @@ -0,0 +1,180 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentResponse_h +#define mozilla_dom_PaymentResponse_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/PaymentResponseBinding.h" // PaymentComplete +#include "nsPIDOMWindow.h" +#include "nsITimer.h" + +namespace mozilla::dom { + +class PaymentAddress; +class PaymentRequest; +struct PaymentValidationErrors; +class Promise; + +class GeneralData final { + public: + GeneralData() = default; + ~GeneralData() = default; + nsString data; +}; + +class BasicCardData final { + public: + struct Address { + nsString country; + CopyableTArray<nsString> addressLine; + nsString region; + nsString regionCode; + nsString city; + nsString dependentLocality; + nsString postalCode; + nsString sortingCode; + nsString organization; + nsString recipient; + nsString phone; + }; + BasicCardData() = default; + ~BasicCardData() = default; + + nsString cardholderName; + nsString cardNumber; + nsString expiryMonth; + nsString expiryYear; + nsString cardSecurityCode; + Address billingAddress; +}; + +class ResponseData final { + public: + enum Type { Unknown = 0, GeneralResponse = 1, BasicCardResponse }; + ResponseData() : mType(ResponseData::Unknown) {} + explicit ResponseData(const GeneralData& aGeneralData) + : mType(GeneralResponse), mGeneralData(aGeneralData) {} + explicit ResponseData(const BasicCardData& aBasicCardData) + : mType(BasicCardResponse), mBasicCardData(aBasicCardData) {} + ResponseData& operator=(const GeneralData& aGeneralData) { + mType = GeneralResponse; + mGeneralData = aGeneralData; + mBasicCardData = BasicCardData(); + return *this; + } + ResponseData& operator=(const BasicCardData& aBasicCardData) { + mType = BasicCardResponse; + mGeneralData = GeneralData(); + mBasicCardData = aBasicCardData; + return *this; + } + ~ResponseData() = default; + + const Type& type() const { return mType; } + const GeneralData& generalData() const { return mGeneralData; } + const BasicCardData& basicCardData() const { return mBasicCardData; } + + private: + Type mType; + GeneralData mGeneralData; + BasicCardData mBasicCardData; +}; + +class PaymentResponse final : public DOMEventTargetHelper, + public nsITimerCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PaymentResponse, + DOMEventTargetHelper) + + NS_IMETHOD Notify(nsITimer* aTimer) override; + + PaymentResponse(nsPIDOMWindowInner* aWindow, PaymentRequest* aRequest, + const nsAString& aRequestId, const nsAString& aMethodName, + const nsAString& aShippingOption, + PaymentAddress* aShippingAddress, + const ResponseData& aDetails, const nsAString& aPayerName, + const nsAString& aPayerEmail, const nsAString& aPayerPhone); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetRequestId(nsString& aRetVal) const; + + void GetMethodName(nsString& aRetVal) const; + + void GetDetails(JSContext* cx, JS::MutableHandle<JSObject*> aRetVal) const; + + already_AddRefed<PaymentAddress> GetShippingAddress() const; + + void GetShippingOption(nsString& aRetVal) const; + + void GetPayerName(nsString& aRetVal) const; + + void GetPayerEmail(nsString& aRetVal) const; + + void GetPayerPhone(nsString& aRetVal) const; + + // Return a raw pointer here to avoid refcounting, but make sure it's safe + // (the object should be kept alive by the callee). + already_AddRefed<Promise> Complete(PaymentComplete result, ErrorResult& aRv); + + void RespondComplete(); + + IMPL_EVENT_HANDLER(payerdetailchange); + + nsresult UpdatePayerDetail(const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone); + + already_AddRefed<Promise> Retry(JSContext* aCx, + const PaymentValidationErrors& errorField, + ErrorResult& aRv); + + void RespondRetry(const nsAString& aMethodName, + const nsAString& aShippingOption, + PaymentAddress* aShippingAddress, + const ResponseData& aDetails, const nsAString& aPayerName, + const nsAString& aPayerEmail, const nsAString& aPayerPhone); + void RejectRetry(ErrorResult&& aRejectReason); + + protected: + ~PaymentResponse(); + + void ValidatePaymentValidationErrors(const PaymentValidationErrors& aErrors, + ErrorResult& aRv); + + void ConvertPaymentMethodErrors(JSContext* aCx, + const PaymentValidationErrors& aErrors, + ErrorResult& aRv) const; + + nsresult DispatchUpdateEvent(const nsAString& aType); + + private: + bool mCompleteCalled; + PaymentRequest* mRequest; + nsString mRequestId; + nsString mMethodName; + ResponseData mDetails; + nsString mShippingOption; + nsString mPayerName; + nsString mPayerEmail; + nsString mPayerPhone; + RefPtr<PaymentAddress> mShippingAddress; + // Promise for "PaymentResponse::Complete" + RefPtr<Promise> mPromise; + // Timer for timing out if the page doesn't call + // complete() + nsCOMPtr<nsITimer> mTimer; + // Promise for "PaymentResponse::Retry" + RefPtr<Promise> mRetryPromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PaymentResponse_h diff --git a/dom/payments/components.conf b/dom/payments/components.conf new file mode 100644 index 0000000000..ec88da3bee --- /dev/null +++ b/dom/payments/components.conf @@ -0,0 +1,80 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{5296f79e-15ea-40c3-8196-19cfa64d328c}', + 'contract_ids': ['@mozilla.org/dom/payments/basiccard-change-details;1'], + 'type': 'mozilla::dom::BasicCardMethodChangeDetails', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'BasicCardMethodChangeDetails'}, + }, + { + 'cid': '{0d55a5e6-d185-44f0-b992-a8e1321e4bce}', + 'contract_ids': ['@mozilla.org/dom/payments/basiccard-response-data;1'], + 'type': 'mozilla::dom::BasicCardResponseData', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'BasicCardResponseData'}, + }, + { + 'cid': '{e031267e-bec8-4f3c-b0b1-396b77ca260c}', + 'contract_ids': ['@mozilla.org/dom/payments/general-change-details;1'], + 'type': 'mozilla::dom::GeneralMethodChangeDetails', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'GeneralMethodChangeDetails'}, + }, + { + 'cid': '{b986773e-2b30-4ed2-b8fe-6a96631c8000}', + 'contract_ids': ['@mozilla.org/dom/payments/general-response-data;1'], + 'type': 'mozilla::dom::GeneralResponseData', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'GeneralResponseData'}, + }, + { + 'cid': '{8c72bcdb-0c37-4786-a9e5-510afa2f8ede}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-abort-action-response;1'], + 'type': 'mozilla::dom::PaymentAbortActionResponse', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'PaymentAbortActionResponse'}, + }, + { + 'cid': '{49a02241-7e48-477a-9345-9f246925dcb3}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-address;1'], + 'type': 'mozilla::dom::payments::PaymentAddress', + 'headers': ['PaymentRequestData.h'], + 'categories': {'payment-request': 'PaymentAddress'}, + }, + { + 'cid': '{52fc3f9f-c0cb-4874-b3d4-ee4b6e9cbe9c}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-canmake-action-response;1'], + 'type': 'mozilla::dom::PaymentCanMakeActionResponse', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'PaymentCanMakeActionResponse'}, + }, + { + 'cid': '{62c01e69-9ca4-4060-99e4-b95f628c8e6d}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-complete-action-response;1'], + 'type': 'mozilla::dom::PaymentCompleteActionResponse', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'PaymentCompleteActionResponse'}, + }, + { + 'cid': '{cccd665f-edf3-41fc-ab9b-fc55b37340aa}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-request-service;1'], + 'singleton': True, + 'type': 'mozilla::dom::PaymentRequestService', + 'headers': ['PaymentRequestService.h'], + 'constructor': 'mozilla::dom::PaymentRequestService::GetSingleton', + 'categories': {'payment-request': 'PaymentRequestService'}, + }, + { + 'cid': '{184385cb-2d35-4b99-a9a3-7c780bf66b9b}', + 'contract_ids': ['@mozilla.org/dom/payments/payment-show-action-response;1'], + 'type': 'mozilla::dom::PaymentShowActionResponse', + 'headers': ['/dom/payments/PaymentActionResponse.h'], + 'categories': {'payment-request': 'PaymentShowActionResponse'}, + }, +] diff --git a/dom/payments/ipc/PPaymentRequest.ipdl b/dom/payments/ipc/PPaymentRequest.ipdl new file mode 100644 index 0000000000..1ac968887c --- /dev/null +++ b/dom/payments/ipc/PPaymentRequest.ipdl @@ -0,0 +1,256 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBrowser; + +include "mozilla/dom/PermissionMessageUtils.h"; + +[RefCounted] using class nsIPrincipal from "nsIPrincipal.h"; + +namespace mozilla { +namespace dom { + +struct IPCPaymentMethodData +{ + nsString supportedMethods; + nsString data; +}; + +struct IPCPaymentCurrencyAmount +{ + nsString currency; + nsString value; +}; + +struct IPCPaymentItem +{ + nsString label; + IPCPaymentCurrencyAmount amount; + bool pending; +}; + +struct IPCPaymentDetailsModifier +{ + nsString supportedMethods; + IPCPaymentItem total; + IPCPaymentItem[] additionalDisplayItems; + nsString data; + bool additionalDisplayItemsPassed; +}; + +struct IPCPaymentShippingOption +{ + nsString id; + nsString label; + IPCPaymentCurrencyAmount amount; + bool selected; +}; + +struct IPCPaymentDetails +{ + nsString id; + IPCPaymentItem total; + IPCPaymentItem[] displayItems; + IPCPaymentShippingOption[] shippingOptions; + IPCPaymentDetailsModifier[] modifiers; + nsString error; + nsString shippingAddressErrors; + nsString payerErrors; + nsString paymentMethodErrors; +}; + +struct IPCPaymentOptions +{ + bool requestPayerName; + bool requestPayerEmail; + bool requestPayerPhone; + bool requestShipping; + bool requestBillingAddress; + nsString shippingType; +}; + +struct IPCPaymentCreateActionRequest +{ + uint64_t topOuterWindowId; + nsString requestId; + nullable nsIPrincipal topLevelPrincipal; + IPCPaymentMethodData[] methodData; + IPCPaymentDetails details; + IPCPaymentOptions options; + nsString shippingOption; +}; + +struct IPCPaymentCanMakeActionRequest +{ + nsString requestId; +}; + +struct IPCPaymentShowActionRequest +{ + nsString requestId; + bool isUpdating; +}; + +struct IPCPaymentAbortActionRequest +{ + nsString requestId; +}; + +struct IPCPaymentCompleteActionRequest +{ + nsString requestId; + nsString completeStatus; +}; + +struct IPCPaymentUpdateActionRequest +{ + nsString requestId; + IPCPaymentDetails details; + nsString shippingOption; +}; + +struct IPCPaymentCloseActionRequest +{ + nsString requestId; +}; + +struct IPCPaymentRetryActionRequest +{ + nsString requestId; + nsString error; + nsString payerErrors; + nsString paymentMethodErrors; + nsString shippingAddressErrors; +}; + +union IPCPaymentActionRequest +{ + IPCPaymentCreateActionRequest; + IPCPaymentCanMakeActionRequest; + IPCPaymentShowActionRequest; + IPCPaymentAbortActionRequest; + IPCPaymentCompleteActionRequest; + IPCPaymentUpdateActionRequest; + IPCPaymentCloseActionRequest; + IPCPaymentRetryActionRequest; +}; + +struct IPCPaymentCanMakeActionResponse +{ + nsString requestId; + bool result; +}; + +struct IPCPaymentAddress +{ + nsString country; + nsString[] addressLine; + nsString region; + nsString regionCode; + nsString city; + nsString dependentLocality; + nsString postalCode; + nsString sortingCode; + nsString organization; + nsString recipient; + nsString phone; +}; + +struct IPCGeneralResponse +{ + nsString data; +}; + +struct IPCBasicCardResponse +{ + nsString cardholderName; + nsString cardNumber; + nsString expiryMonth; + nsString expiryYear; + nsString cardSecurityCode; + IPCPaymentAddress billingAddress; +}; + +union IPCPaymentResponseData +{ + IPCGeneralResponse; + IPCBasicCardResponse; +}; + +struct IPCPaymentShowActionResponse +{ + nsString requestId; + uint32_t status; + nsString methodName; + IPCPaymentResponseData data; + nsString payerName; + nsString payerEmail; + nsString payerPhone; +}; + +struct IPCPaymentAbortActionResponse +{ + nsString requestId; + bool isSucceeded; +}; + +struct IPCPaymentCompleteActionResponse +{ + nsString requestId; + bool isCompleted; +}; + +union IPCPaymentActionResponse +{ + IPCPaymentCanMakeActionResponse; + IPCPaymentShowActionResponse; + IPCPaymentAbortActionResponse; + IPCPaymentCompleteActionResponse; +}; + +struct IPCGeneralChangeDetails +{ + nsString details; +}; + +struct IPCBasicCardChangeDetails +{ + IPCPaymentAddress billingAddress; +}; + +union IPCMethodChangeDetails +{ + IPCGeneralChangeDetails; + IPCBasicCardChangeDetails; +}; + +[ManualDealloc] +sync protocol PPaymentRequest +{ + manager PBrowser; + +parent: + async __delete__(); + + async RequestPayment(IPCPaymentActionRequest aAction); + +child: + async RespondPayment(IPCPaymentActionResponse aResponse); + async ChangeShippingAddress(nsString aRequestId, + IPCPaymentAddress aAddress); + async ChangeShippingOption(nsString aRequestId, + nsString aOption); + async ChangePayerDetail(nsString aRequestId, + nsString aPayerName, + nsString aPayerEmail, + nsString aPayerPhone); + async ChangePaymentMethod(nsString aRequestId, + nsString aMethodName, + IPCMethodChangeDetails aMethodDetails); +}; + +} // end of namespace dom +} // end of namespace mozilla diff --git a/dom/payments/ipc/PaymentRequestChild.cpp b/dom/payments/ipc/PaymentRequestChild.cpp new file mode 100644 index 0000000000..a3a22bf346 --- /dev/null +++ b/dom/payments/ipc/PaymentRequestChild.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PaymentRequestChild.h" +#include "mozilla/dom/PaymentRequest.h" +#include "mozilla/dom/PaymentRequestManager.h" + +namespace mozilla::dom { + +PaymentRequestChild::PaymentRequestChild(PaymentRequest* aRequest) + : mRequest(aRequest) { + mRequest->SetIPC(this); +} + +nsresult PaymentRequestChild::RequestPayment( + const IPCPaymentActionRequest& aAction) { + if (!mRequest) { + return NS_ERROR_FAILURE; + } + if (!SendRequestPayment(aAction)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +mozilla::ipc::IPCResult PaymentRequestChild::RecvRespondPayment( + const IPCPaymentActionResponse& aResponse) { + if (!mRequest) { + return IPC_FAIL_NO_REASON(this); + } + const IPCPaymentActionResponse& response = aResponse; + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + + // Hold a strong reference to our request for the entire response. + RefPtr<PaymentRequest> request(mRequest); + nsresult rv = manager->RespondPayment(request, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult PaymentRequestChild::RecvChangeShippingAddress( + const nsString& aRequestId, const IPCPaymentAddress& aAddress) { + if (!mRequest) { + return IPC_FAIL_NO_REASON(this); + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + RefPtr<PaymentRequest> request(mRequest); + nsresult rv = manager->ChangeShippingAddress(request, aAddress); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult PaymentRequestChild::RecvChangeShippingOption( + const nsString& aRequestId, const nsString& aOption) { + if (!mRequest) { + return IPC_FAIL_NO_REASON(this); + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + RefPtr<PaymentRequest> request(mRequest); + nsresult rv = manager->ChangeShippingOption(request, aOption); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult PaymentRequestChild::RecvChangePayerDetail( + const nsString& aRequestId, const nsString& aPayerName, + const nsString& aPayerEmail, const nsString& aPayerPhone) { + if (!mRequest) { + return IPC_FAIL_NO_REASON(this); + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + RefPtr<PaymentRequest> request(mRequest); + nsresult rv = + manager->ChangePayerDetail(request, aPayerName, aPayerEmail, aPayerPhone); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult PaymentRequestChild::RecvChangePaymentMethod( + const nsString& aRequestId, const nsString& aMethodName, + const IPCMethodChangeDetails& aMethodDetails) { + if (!mRequest) { + return IPC_FAIL_NO_REASON(this); + } + RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + RefPtr<PaymentRequest> request(mRequest); + nsresult rv = + manager->ChangePaymentMethod(request, aMethodName, aMethodDetails); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +void PaymentRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + if (mRequest) { + DetachFromRequest(true); + } +} + +void PaymentRequestChild::MaybeDelete(bool aCanBeInManager) { + if (mRequest) { + DetachFromRequest(aCanBeInManager); + Send__delete__(this); + } +} + +void PaymentRequestChild::DetachFromRequest(bool aCanBeInManager) { + MOZ_ASSERT(mRequest); + + if (aCanBeInManager) { + RefPtr<PaymentRequestManager> manager = + PaymentRequestManager::GetSingleton(); + MOZ_ASSERT(manager); + + RefPtr<PaymentRequest> request(mRequest); + manager->RequestIPCOver(request); + } + + mRequest->SetIPC(nullptr); + mRequest = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/payments/ipc/PaymentRequestChild.h b/dom/payments/ipc/PaymentRequestChild.h new file mode 100644 index 0000000000..4047d2c055 --- /dev/null +++ b/dom/payments/ipc/PaymentRequestChild.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestChild_h +#define mozilla_dom_PaymentRequestChild_h + +#include "mozilla/dom/PPaymentRequestChild.h" + +namespace mozilla::dom { + +class PaymentRequest; + +class PaymentRequestChild final : public PPaymentRequestChild { + friend class PPaymentRequestChild; + + public: + explicit PaymentRequestChild(PaymentRequest* aRequest); + + void MaybeDelete(bool aCanBeInManager); + + nsresult RequestPayment(const IPCPaymentActionRequest& aAction); + + protected: + mozilla::ipc::IPCResult RecvRespondPayment( + const IPCPaymentActionResponse& aResponse); + + mozilla::ipc::IPCResult RecvChangeShippingAddress( + const nsString& aRequestId, const IPCPaymentAddress& aAddress); + + mozilla::ipc::IPCResult RecvChangeShippingOption(const nsString& aRequestId, + const nsString& aOption); + + mozilla::ipc::IPCResult RecvChangePayerDetail(const nsString& aRequestId, + const nsString& aPayerName, + const nsString& aPayerEmail, + const nsString& aPayerPhone); + + mozilla::ipc::IPCResult RecvChangePaymentMethod( + const nsString& aRequestId, const nsString& aMethodName, + const IPCMethodChangeDetails& aMethodDetails); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~PaymentRequestChild() = default; + + void DetachFromRequest(bool aCanBeInManager); + + PaymentRequest* MOZ_NON_OWNING_REF mRequest; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/ipc/PaymentRequestParent.cpp b/dom/payments/ipc/PaymentRequestParent.cpp new file mode 100644 index 0000000000..38a41ec9b1 --- /dev/null +++ b/dom/payments/ipc/PaymentRequestParent.cpp @@ -0,0 +1,468 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ipc/InputStreamUtils.h" +#include "nsArrayUtils.h" +#include "nsCOMPtr.h" +#include "nsIPaymentRequestService.h" +#include "nsISupportsPrimitives.h" +#include "nsServiceManagerUtils.h" +#include "PaymentRequestData.h" +#include "PaymentRequestParent.h" +#include "PaymentRequestService.h" + +namespace mozilla::dom { + +PaymentRequestParent::PaymentRequestParent() + : mActorAlive(true), mRequestId(u""_ns) {} + +mozilla::ipc::IPCResult PaymentRequestParent::RecvRequestPayment( + const IPCPaymentActionRequest& aRequest) { + if (!mActorAlive) { + return IPC_FAIL_NO_REASON(this); + } + switch (aRequest.type()) { + case IPCPaymentActionRequest::TIPCPaymentCreateActionRequest: { + const IPCPaymentCreateActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentCanMakeActionRequest: { + const IPCPaymentCanMakeActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentShowActionRequest: { + const IPCPaymentShowActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentAbortActionRequest: { + const IPCPaymentAbortActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentCompleteActionRequest: { + const IPCPaymentCompleteActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentUpdateActionRequest: { + const IPCPaymentUpdateActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentCloseActionRequest: { + const IPCPaymentCloseActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + case IPCPaymentActionRequest::TIPCPaymentRetryActionRequest: { + const IPCPaymentRetryActionRequest& request = aRequest; + mRequestId = request.requestId(); + break; + } + default: { + return IPC_FAIL(this, "Unknown PaymentRequest action type"); + } + } + nsCOMPtr<nsIPaymentRequestService> service = + do_GetService(NS_PAYMENT_REQUEST_SERVICE_CONTRACT_ID); + MOZ_ASSERT(service); + PaymentRequestService* rowService = + static_cast<PaymentRequestService*>(service.get()); + MOZ_ASSERT(rowService); + nsresult rv = rowService->RequestPayment(mRequestId, aRequest, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL(this, "nsIPaymentRequestService::RequestPayment failed"); + } + return IPC_OK(); +} + +nsresult PaymentRequestParent::RespondPayment( + nsIPaymentActionResponse* aResponse) { + if (!NS_IsMainThread()) { + RefPtr<PaymentRequestParent> self = this; + nsCOMPtr<nsIPaymentActionResponse> response = aResponse; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "PaymentRequestParent::RespondPayment", + [self, response]() { self->RespondPayment(response); }); + return NS_DispatchToMainThread(r); + } + + if (!mActorAlive) { + return NS_ERROR_FAILURE; + } + uint32_t type; + nsresult rv = aResponse->GetType(&type); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString requestId; + rv = aResponse->GetRequestId(requestId); + NS_ENSURE_SUCCESS(rv, rv); + switch (type) { + case nsIPaymentActionResponse::CANMAKE_ACTION: { + nsCOMPtr<nsIPaymentCanMakeActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + bool result; + rv = response->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + IPCPaymentCanMakeActionResponse actionResponse(requestId, result); + if (!SendRespondPayment(actionResponse)) { + return NS_ERROR_FAILURE; + } + break; + } + case nsIPaymentActionResponse::SHOW_ACTION: { + nsCOMPtr<nsIPaymentShowActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + uint32_t acceptStatus; + NS_ENSURE_SUCCESS(response->GetAcceptStatus(&acceptStatus), + NS_ERROR_FAILURE); + nsAutoString methodName; + NS_ENSURE_SUCCESS(response->GetMethodName(methodName), NS_ERROR_FAILURE); + IPCPaymentResponseData ipcData; + if (acceptStatus == nsIPaymentActionResponse::PAYMENT_ACCEPTED) { + nsCOMPtr<nsIPaymentResponseData> data; + NS_ENSURE_SUCCESS(response->GetData(getter_AddRefs(data)), + NS_ERROR_FAILURE); + MOZ_ASSERT(data); + NS_ENSURE_SUCCESS(SerializeResponseData(ipcData, data), + NS_ERROR_FAILURE); + } else { + ipcData = IPCGeneralResponse(); + } + + nsAutoString payerName; + NS_ENSURE_SUCCESS(response->GetPayerName(payerName), NS_ERROR_FAILURE); + nsAutoString payerEmail; + NS_ENSURE_SUCCESS(response->GetPayerEmail(payerEmail), NS_ERROR_FAILURE); + nsAutoString payerPhone; + NS_ENSURE_SUCCESS(response->GetPayerPhone(payerPhone), NS_ERROR_FAILURE); + IPCPaymentShowActionResponse actionResponse( + requestId, acceptStatus, methodName, ipcData, payerName, payerEmail, + payerPhone); + if (!SendRespondPayment(actionResponse)) { + return NS_ERROR_FAILURE; + } + break; + } + case nsIPaymentActionResponse::ABORT_ACTION: { + nsCOMPtr<nsIPaymentAbortActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + bool isSucceeded; + rv = response->IsSucceeded(&isSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + IPCPaymentAbortActionResponse actionResponse(requestId, isSucceeded); + if (!SendRespondPayment(actionResponse)) { + return NS_ERROR_FAILURE; + } + break; + } + case nsIPaymentActionResponse::COMPLETE_ACTION: { + nsCOMPtr<nsIPaymentCompleteActionResponse> response = + do_QueryInterface(aResponse); + MOZ_ASSERT(response); + bool isCompleted; + rv = response->IsCompleted(&isCompleted); + NS_ENSURE_SUCCESS(rv, rv); + IPCPaymentCompleteActionResponse actionResponse(requestId, isCompleted); + if (!SendRespondPayment(actionResponse)) { + return NS_ERROR_FAILURE; + } + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +nsresult PaymentRequestParent::ChangeShippingAddress( + const nsAString& aRequestId, nsIPaymentAddress* aAddress) { + if (!NS_IsMainThread()) { + RefPtr<PaymentRequestParent> self = this; + nsCOMPtr<nsIPaymentAddress> address = aAddress; + nsAutoString requestId(aRequestId); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "dom::PaymentRequestParent::ChangeShippingAddress", + [self, requestId, address]() { + self->ChangeShippingAddress(requestId, address); + }); + return NS_DispatchToMainThread(r); + } + if (!mActorAlive) { + return NS_ERROR_FAILURE; + } + + IPCPaymentAddress ipcAddress; + nsresult rv = SerializeAddress(ipcAddress, aAddress); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString requestId(aRequestId); + if (!SendChangeShippingAddress(requestId, ipcAddress)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult PaymentRequestParent::ChangeShippingOption(const nsAString& aRequestId, + const nsAString& aOption) { + if (!NS_IsMainThread()) { + RefPtr<PaymentRequestParent> self = this; + nsAutoString requestId(aRequestId); + nsAutoString option(aOption); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "dom::PaymentRequestParent::ChangeShippingOption", + [self, requestId, option]() { + self->ChangeShippingOption(requestId, option); + }); + return NS_DispatchToMainThread(r); + } + if (!mActorAlive) { + return NS_ERROR_FAILURE; + } + nsAutoString requestId(aRequestId); + nsAutoString option(aOption); + if (!SendChangeShippingOption(requestId, option)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult PaymentRequestParent::ChangePayerDetail(const nsAString& aRequestId, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone) { + nsAutoString requestId(aRequestId); + nsAutoString payerName(aPayerName); + nsAutoString payerEmail(aPayerEmail); + nsAutoString payerPhone(aPayerPhone); + if (!NS_IsMainThread()) { + RefPtr<PaymentRequestParent> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "dom::PaymentRequestParent::ChangePayerDetail", + [self, requestId, payerName, payerEmail, payerPhone]() { + self->ChangePayerDetail(requestId, payerName, payerEmail, payerPhone); + }); + return NS_DispatchToMainThread(r); + } + if (!mActorAlive) { + return NS_ERROR_FAILURE; + } + if (!SendChangePayerDetail(requestId, payerName, payerEmail, payerPhone)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult PaymentRequestParent::ChangePaymentMethod( + const nsAString& aRequestId, const nsAString& aMethodName, + nsIMethodChangeDetails* aMethodDetails) { + nsAutoString requestId(aRequestId); + nsAutoString methodName(aMethodName); + nsCOMPtr<nsIMethodChangeDetails> methodDetails(aMethodDetails); + if (!NS_IsMainThread()) { + RefPtr<PaymentRequestParent> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "dom::PaymentRequestParent::ChangePaymentMethod", + [self, requestId, methodName, methodDetails]() { + self->ChangePaymentMethod(requestId, methodName, methodDetails); + }); + return NS_DispatchToMainThread(r); + } + if (!mActorAlive) { + return NS_ERROR_FAILURE; + } + + // Convert nsIMethodChangeDetails to IPCMethodChangeDetails + // aMethodChangeDetails can be null + IPCMethodChangeDetails ipcChangeDetails; + if (aMethodDetails) { + uint32_t dataType; + NS_ENSURE_SUCCESS(aMethodDetails->GetType(&dataType), NS_ERROR_FAILURE); + switch (dataType) { + case nsIMethodChangeDetails::GENERAL_DETAILS: { + nsCOMPtr<nsIGeneralChangeDetails> details = + do_QueryInterface(methodDetails); + MOZ_ASSERT(details); + IPCGeneralChangeDetails ipcGeneralDetails; + NS_ENSURE_SUCCESS(details->GetDetails(ipcGeneralDetails.details()), + NS_ERROR_FAILURE); + ipcChangeDetails = ipcGeneralDetails; + break; + } + case nsIMethodChangeDetails::BASICCARD_DETAILS: { + nsCOMPtr<nsIBasicCardChangeDetails> details = + do_QueryInterface(methodDetails); + MOZ_ASSERT(details); + IPCBasicCardChangeDetails ipcBasicCardDetails; + nsCOMPtr<nsIPaymentAddress> address; + NS_ENSURE_SUCCESS(details->GetBillingAddress(getter_AddRefs(address)), + NS_ERROR_FAILURE); + IPCPaymentAddress ipcAddress; + NS_ENSURE_SUCCESS(SerializeAddress(ipcAddress, address), + NS_ERROR_FAILURE); + ipcBasicCardDetails.billingAddress() = ipcAddress; + ipcChangeDetails = ipcBasicCardDetails; + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + } + if (!SendChangePaymentMethod(requestId, methodName, ipcChangeDetails)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +mozilla::ipc::IPCResult PaymentRequestParent::Recv__delete__() { + mActorAlive = false; + return IPC_OK(); +} + +void PaymentRequestParent::ActorDestroy(ActorDestroyReason aWhy) { + mActorAlive = false; + nsCOMPtr<nsIPaymentRequestService> service = + do_GetService(NS_PAYMENT_REQUEST_SERVICE_CONTRACT_ID); + MOZ_ASSERT(service); + if (!mRequestId.Equals(u""_ns)) { + nsCOMPtr<nsIPaymentRequest> request; + nsresult rv = + service->GetPaymentRequestById(mRequestId, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + if (!request) { + return; + } + payments::PaymentRequest* rowRequest = + static_cast<payments::PaymentRequest*>(request.get()); + MOZ_ASSERT(rowRequest); + rowRequest->SetIPC(nullptr); + } +} + +nsresult PaymentRequestParent::SerializeAddress(IPCPaymentAddress& aIPCAddress, + nsIPaymentAddress* aAddress) { + // address can be nullptr + if (!aAddress) { + return NS_OK; + } + nsAutoString country; + nsresult rv = aAddress->GetCountry(country); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIArray> iaddressLine; + rv = aAddress->GetAddressLine(getter_AddRefs(iaddressLine)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString region; + rv = aAddress->GetRegion(region); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString regionCode; + rv = aAddress->GetRegionCode(regionCode); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString city; + rv = aAddress->GetCity(city); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString dependentLocality; + rv = aAddress->GetDependentLocality(dependentLocality); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString postalCode; + rv = aAddress->GetPostalCode(postalCode); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString sortingCode; + rv = aAddress->GetSortingCode(sortingCode); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString organization; + rv = aAddress->GetOrganization(organization); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString recipient; + rv = aAddress->GetRecipient(recipient); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString phone; + rv = aAddress->GetPhone(phone); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<nsString> addressLine; + uint32_t length; + rv = iaddressLine->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t index = 0; index < length; ++index) { + nsCOMPtr<nsISupportsString> iaddress = + do_QueryElementAt(iaddressLine, index); + MOZ_ASSERT(iaddress); + nsAutoString address; + rv = iaddress->GetData(address); + NS_ENSURE_SUCCESS(rv, rv); + addressLine.AppendElement(address); + } + + aIPCAddress = IPCPaymentAddress(country, addressLine, region, regionCode, + city, dependentLocality, postalCode, + sortingCode, organization, recipient, phone); + return NS_OK; +} + +nsresult PaymentRequestParent::SerializeResponseData( + IPCPaymentResponseData& aIPCData, nsIPaymentResponseData* aData) { + NS_ENSURE_ARG_POINTER(aData); + uint32_t dataType; + NS_ENSURE_SUCCESS(aData->GetType(&dataType), NS_ERROR_FAILURE); + switch (dataType) { + case nsIPaymentResponseData::GENERAL_RESPONSE: { + nsCOMPtr<nsIGeneralResponseData> response = do_QueryInterface(aData); + MOZ_ASSERT(response); + IPCGeneralResponse data; + NS_ENSURE_SUCCESS(response->GetData(data.data()), NS_ERROR_FAILURE); + aIPCData = data; + break; + } + case nsIPaymentResponseData::BASICCARD_RESPONSE: { + nsCOMPtr<nsIBasicCardResponseData> response = do_QueryInterface(aData); + MOZ_ASSERT(response); + IPCBasicCardResponse data; + NS_ENSURE_SUCCESS(response->GetCardholderName(data.cardholderName()), + NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(response->GetCardNumber(data.cardNumber()), + NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(response->GetExpiryMonth(data.expiryMonth()), + NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(response->GetExpiryYear(data.expiryYear()), + NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(response->GetCardSecurityCode(data.cardSecurityCode()), + NS_ERROR_FAILURE); + nsCOMPtr<nsIPaymentAddress> address; + NS_ENSURE_SUCCESS(response->GetBillingAddress(getter_AddRefs(address)), + NS_ERROR_FAILURE); + IPCPaymentAddress ipcAddress; + NS_ENSURE_SUCCESS(SerializeAddress(ipcAddress, address), + NS_ERROR_FAILURE); + data.billingAddress() = ipcAddress; + aIPCData = data; + break; + } + default: { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} +} // namespace mozilla::dom diff --git a/dom/payments/ipc/PaymentRequestParent.h b/dom/payments/ipc/PaymentRequestParent.h new file mode 100644 index 0000000000..5bcab7b204 --- /dev/null +++ b/dom/payments/ipc/PaymentRequestParent.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PaymentRequestParent_h +#define mozilla_dom_PaymentRequestParent_h + +#include "mozilla/dom/PPaymentRequestParent.h" +#include "nsIPaymentAddress.h" +#include "nsIPaymentActionResponse.h" + +namespace mozilla::dom { + +class PaymentRequestParent final : public PPaymentRequestParent { + friend class PPaymentRequestParent; + + NS_INLINE_DECL_REFCOUNTING(PaymentRequestParent) + public: + PaymentRequestParent(); + + nsresult RespondPayment(nsIPaymentActionResponse* aResponse); + nsresult ChangeShippingAddress(const nsAString& aRequestId, + nsIPaymentAddress* aAddress); + nsresult ChangeShippingOption(const nsAString& aRequestId, + const nsAString& aOption); + nsresult ChangePayerDetail(const nsAString& aRequestId, + const nsAString& aPayerName, + const nsAString& aPayerEmail, + const nsAString& aPayerPhone); + nsresult ChangePaymentMethod(const nsAString& aRequestId, + const nsAString& aMethodName, + nsIMethodChangeDetails* aMethodDetails); + + protected: + mozilla::ipc::IPCResult RecvRequestPayment( + const IPCPaymentActionRequest& aRequest); + + mozilla::ipc::IPCResult Recv__delete__() override; + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~PaymentRequestParent() = default; + + nsresult SerializeAddress(IPCPaymentAddress& ipcAddress, + nsIPaymentAddress* aAddress); + nsresult SerializeResponseData(IPCPaymentResponseData& ipcData, + nsIPaymentResponseData* aData); + + bool mActorAlive; + nsString mRequestId; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/payments/ipc/moz.build b/dom/payments/ipc/moz.build new file mode 100644 index 0000000000..83c5afd577 --- /dev/null +++ b/dom/payments/ipc/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "PaymentRequestChild.h", + "PaymentRequestParent.h", +] + +UNIFIED_SOURCES += [ + "PaymentRequestChild.cpp", + "PaymentRequestParent.cpp", +] + +IPDL_SOURCES += [ + "PPaymentRequest.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/payments/moz.build b/dom/payments/moz.build new file mode 100644 index 0000000000..9db02c2d1f --- /dev/null +++ b/dom/payments/moz.build @@ -0,0 +1,53 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "ipc", +] + +EXPORTS += [ + "PaymentRequestData.h", + "PaymentRequestService.h", +] + +EXPORTS.mozilla.dom += [ + "MerchantValidationEvent.h", + "PaymentAddress.h", + "PaymentMethodChangeEvent.h", + "PaymentRequest.h", + "PaymentRequestManager.h", + "PaymentRequestUpdateEvent.h", + "PaymentResponse.h", +] + +UNIFIED_SOURCES += [ + "BasicCardPayment.cpp", + "MerchantValidationEvent.cpp", + "PaymentActionResponse.cpp", + "PaymentAddress.cpp", + "PaymentMethodChangeEvent.cpp", + "PaymentRequest.cpp", + "PaymentRequestData.cpp", + "PaymentRequestManager.cpp", + "PaymentRequestService.cpp", + "PaymentRequestUpdateEvent.cpp", + "PaymentRequestUtils.cpp", + "PaymentResponse.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Web Payments") + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] diff --git a/dom/payments/test/BasicCardErrorsChromeScript.js b/dom/payments/test/BasicCardErrorsChromeScript.js new file mode 100644 index 0000000000..f92e5eef5c --- /dev/null +++ b/dom/payments/test/BasicCardErrorsChromeScript.js @@ -0,0 +1,133 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +const defaultCard = { + cardholderName: "", + cardNumber: "4111111111111111", + expiryMonth: "", + expiryYear: "", + cardSecurityCode: "", + billingAddress: null, +}; + +function makeBillingAddress() { + const billingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" + ].createInstance(Ci.nsIPaymentAddress); + const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + address.data = "Easton Ave"; + addressLine.appendElement(address); + const addressArgs = [ + "USA", // country + addressLine, // address line + "CA", // region + "CA", // regionCode + "San Bruno", // city + "", // dependent locality + "94066", // postal code + "123456", // sorting code + "", // organization + "Bill A. Pacheco", // recipient + "+14344413879", // phone + ]; + billingAddress.init(...addressArgs); + return billingAddress; +} + +function makeBasicCardResponse(details) { + const basicCardResponseData = Cc[ + "@mozilla.org/dom/payments/basiccard-response-data;1" + ].createInstance(Ci.nsIBasicCardResponseData); + const { + cardholderName, + cardNumber, + expiryMonth, + expiryYear, + cardSecurityCode, + billingAddress, + } = details; + + const address = + billingAddress !== undefined ? billingAddress : makeBillingAddress(); + + basicCardResponseData.initData( + cardholderName, + cardNumber, + expiryMonth, + expiryYear, + cardSecurityCode, + address + ); + + return basicCardResponseData; +} + +const TestingUIService = { + showPayment(requestId, details = { ...defaultCard }) { + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + makeBasicCardResponse(details), + "Person name", + "Person email", + "Person phone" + ); + + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + // Handles response.retry({ paymentMethod }): + updatePayment(requestId) { + // Let's echo what was sent in by the error... + const request = paymentSrv.getPaymentRequestById(requestId); + this.showPayment(requestId, request.paymentDetails.paymentMethodErrors); + }, + // Handles response.complete() + completePayment(requestId) { + const request = paymentSrv.getPaymentRequestById(requestId); + const completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + get QueryInterface() { + return ChromeUtils.generateQI(["nsIPaymentUIService"]); + }, +}; + +paymentSrv.setTestingUIService( + TestingUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("teardown", () => { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/BasiccardChromeScript.js b/dom/payments/test/BasiccardChromeScript.js new file mode 100644 index 0000000000..6ce2ca024b --- /dev/null +++ b/dom/payments/test/BasiccardChromeScript.js @@ -0,0 +1,372 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", `${DummyUIService.testName}: ${message}`); +} + +const billingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +billingAddress.init( + "USA", // country + addressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "", // dependent locality + "94066", // postal code + "123456", // sorting code + "", // organization + "Bill A. Pacheco", // recipient + "+14344413879" +); // phone + +const specialAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const specialAddressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const specialData = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +specialData.data = ":$%@&*"; +specialAddressLine.appendElement(specialData); +specialAddress.init( + "USA", // country + specialAddressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "", // dependent locality + "94066", // postal code + "123456", // sorting code + "", // organization + "Bill A. Pacheco", // recipient + "+14344413879" +); // phone + +const basiccardResponseData = Cc[ + "@mozilla.org/dom/payments/basiccard-response-data;1" +].createInstance(Ci.nsIBasicCardResponseData); + +const basiccardChangeDetails = Cc[ + "@mozilla.org/dom/payments/basiccard-change-details;1" +].createInstance(Ci.nsIBasicCardChangeDetails); + +const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" +].createInstance(Ci.nsIPaymentShowActionResponse); + +function abortPaymentResponse(requestId) { + let abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED); + paymentSrv.respondPayment( + abortResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function completePaymentResponse(requestId) { + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function showRequest(requestId) { + if (DummyUIService.showAction === "payment-method-change") { + basiccardChangeDetails.initData(billingAddress); + try { + paymentSrv.changePaymentMethod( + requestId, + "basic-card", + basiccardChangeDetails.QueryInterface(Ci.nsIMethodChangeDetails) + ); + } catch (error) { + emitTestFail( + `Unexpected error (${error.name}) when calling PaymentRequestService::changePaymentMethod` + ); + } + return; + } + if (DummyUIService.showAction === "detailBasicCardResponse") { + try { + basiccardResponseData.initData( + "Bill A. Pacheco", // cardholderName + "4916855166538720", // cardNumber + "01", // expiryMonth + "2024", // expiryYear + "180", // cardSecurityCode + billingAddress + ); // billingAddress + } catch (e) { + emitTestFail("Fail to initialize basic card response data."); + } + } + if (DummyUIService.showAction === "simpleBasicCardResponse") { + try { + basiccardResponseData.initData( + "", // cardholderName + "4916855166538720", // cardNumber + "", // expiryMonth + "", // expiryYear + "", // cardSecurityCode + null + ); // billingAddress + } catch (e) { + emitTestFail("Fail to initialize basic card response data."); + } + } + if (DummyUIService.showAction === "specialAddressResponse") { + try { + basiccardResponseData.initData( + "Bill A. Pacheco", // cardholderName + "4916855166538720", // cardNumber + "01", // expiryMonth + "2024", // expiryYear + "180", // cardSecurityCode + specialAddress + ); // billingAddress + } catch (e) { + emitTestFail("Fail to initialize basic card response data."); + } + } + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + basiccardResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +const DummyUIService = { + testName: "", + showAction: "", + showPayment: showRequest, + abortPayment: abortPaymentResponse, + completePayment: completePaymentResponse, + updatePayment: requestId => { + try { + basiccardResponseData.initData( + "Bill A. Pacheco", // cardholderName + "4916855166538720", // cardNumber + "01", // expiryMonth + "2024", // expiryYear + "180", // cardSecurityCode + billingAddress + ); // billingAddress + } catch (e) { + emitTestFail("Fail to initialize basic card response data."); + } + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + basiccardResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + closePayment: requestId => {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("set-detailed-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.showAction = "detailBasicCardResponse"; + sendAsyncMessage("set-detailed-ui-service-complete"); +}); + +addMessageListener("set-simple-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.showAction = "simpleBasicCardResponse"; + sendAsyncMessage("set-simple-ui-service-complete"); +}); + +addMessageListener("set-special-address-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.showAction = "specialAddressResponse"; + sendAsyncMessage("set-special-address-ui-service-complete"); +}); + +addMessageListener("method-change-to-basic-card", function (testName) { + DummyUIService.testName = testName; + DummyUIService.showAction = "payment-method-change"; + sendAsyncMessage("method-change-to-basic-card-complete"); +}); + +addMessageListener("error-response-test", function (testName) { + // test empty cardNumber + try { + basiccardResponseData.initData("", "", "", "", "", null); + emitTestFail( + "BasicCardResponse should not be initialized with empty cardNumber." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "Empty cardNumber expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + + // test invalid expiryMonth 123 + try { + basiccardResponseData.initData("", "4916855166538720", "123", "", "", null); + emitTestFail( + "BasicCardResponse should not be initialized with invalid expiryMonth '123'." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "expiryMonth 123 expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + // test invalid expiryMonth 99 + try { + basiccardResponseData.initData("", "4916855166538720", "99", "", "", null); + emitTestFail( + "BasicCardResponse should not be initialized with invalid expiryMonth '99'." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "expiryMonth 99 xpected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + // test invalid expiryMonth ab + try { + basiccardResponseData.initData("", "4916855166538720", "ab", "", "", null); + emitTestFail( + "BasicCardResponse should not be initialized with invalid expiryMonth 'ab'." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "expiryMonth ab expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + // test invalid expiryYear abcd + try { + basiccardResponseData.initData( + "", + "4916855166538720", + "", + "abcd", + "", + null + ); + emitTestFail( + "BasicCardResponse should not be initialized with invalid expiryYear 'abcd'." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "expiryYear abcd expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + // test invalid expiryYear 11111 + try { + basiccardResponseData.initData( + "", + "4916855166538720", + "", + "11111", + "", + null + ); + emitTestFail( + "BasicCardResponse should not be initialized with invalid expiryYear '11111'." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "expiryYear 11111 expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + try { + responseData.initData({}); + } catch (e) { + emitTestFail("Fail to initialize response data with empty object."); + } + + try { + showResponse.init( + "testid", + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + responseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + emitTestFail( + "nsIPaymentShowActionResponse should not be initialized with basic-card method and nsIGeneralResponseData." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "ShowResponse init expected 'NS_ERROR_FAILURE', but got " + e.name + "." + ); + } + } + sendAsyncMessage("error-response-test-complete"); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/Bug1478740ChromeScript.js b/dom/payments/test/Bug1478740ChromeScript.js new file mode 100644 index 0000000000..6e8633ff87 --- /dev/null +++ b/dom/payments/test/Bug1478740ChromeScript.js @@ -0,0 +1,90 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", message); +} + +function rejectPayment(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +const DummyUIService = { + testName: "", + requestId: "", + showPayment(requestId) { + this.requestId = requestId; + sendAsyncMessage("showing-payment", { data: "successful" }); + }, + abortPayment(requestId) { + this.requestId = requestId; + }, + completePayment(requestId) { + this.requestId = requestId; + }, + updatePayment(requestId) { + this.requestId = requestId; + }, + closePayment(requestId) { + this.requestId = requestId; + }, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("reject-payment", function () { + rejectPayment(DummyUIService.requestId); + sendAsyncMessage("reject-payment-complete"); +}); + +addMessageListener("start-test", function (testName) { + DummyUIService.testName = testName; + sendAsyncMessage("start-test-complete"); +}); + +addMessageListener("finish-test", function () { + DummyUIService.testName = ""; + sendAsyncMessage("finish-test-complete"); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/Bug1490698ChromeScript.js b/dom/payments/test/Bug1490698ChromeScript.js new file mode 100644 index 0000000000..dbae51b117 --- /dev/null +++ b/dom/payments/test/Bug1490698ChromeScript.js @@ -0,0 +1,226 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", message); +} + +const billingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +billingAddress.init( + "USA", // country + addressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "", // dependent locality + "94066", // postal code + "123456", // sorting code + "", // organization + "Bill A. Pacheco", // recipient + "+14344413879" +); // phone + +function acceptPayment(requestId) { + const basiccardResponseData = Cc[ + "@mozilla.org/dom/payments/basiccard-response-data;1" + ].createInstance(Ci.nsIBasicCardResponseData); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + basiccardResponseData.initData( + "Bill A. Pacheco", // cardholderName + "4916855166538720", // cardNumber + "01", // expiryMonth + "2024", // expiryYear + "180", // cardSecurityCode + billingAddress + ); // billingAddress + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + basiccardResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function rejectPayment(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +const DummyUIService = { + testName: "", + requestId: "", + showPayment(requestId) { + this.requestId = requestId; + acceptPayment(requestId); + }, + abortPaymen(requestId) { + this.requestId = requestId; + }, + completePayment(requestId) { + this.requestId = requestId; + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + updatePayment(requestId) { + this.requestId = requestId; + }, + closePayment(requestId) { + this.requestId = requestId; + }, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("start-test", function (testName) { + DummyUIService.testName = testName; + sendAsyncMessage("start-test-complete"); +}); + +addMessageListener("finish-test", function () { + DummyUIService.testName = ""; + sendAsyncMessage("finish-test-complete"); +}); + +addMessageListener("interact-with-payment", function () { + if (DummyUIService.requestId === "") { + emitTestFail(`${DummyUIService.testName}: Unexpected empty requestId`); + } + try { + acceptPayment(DummyUIService.requestId); + emitTestFail( + `${DummyUIService.testName}: Got unexpected success when accepting PaymentRequest.` + ); + } catch (err) { + if (err.name !== "NS_ERROR_FAILURE") { + emitTestFail( + `${DummyUIService.testName}: Got unexpected '${err.name}' when accepting PaymentRequest.` + ); + } else { + emitTestPass( + `${DummyUIService.testName}: Got expected 'NS_ERROR_FAILURE' when accepting PaymentRequest.` + ); + } + } + + try { + rejectPayment(DummyUIService.requestId); + emitTestFail( + `${DummyUIService.testName}: Got unexpected success when rejecting PaymentRequest.` + ); + } catch (err) { + if (err.name !== "NS_ERROR_FAILURE") { + emitTestFail( + `${DummyUIService.testName}: Got unexpected '${err.name}' when rejecting PaymentRequest.` + ); + } else { + emitTestPass( + `${DummyUIService.testName}: Got expected 'NS_ERROR_FAILURE' when rejecting PaymentRequest.` + ); + } + } + + try { + paymentSrv.changeShippingOption( + DummyUIService.requestId, + "error shippping option" + ); + emitTestFail( + `${DummyUIService.testName}: Got unexpected success when changing shippingOption.` + ); + } catch (err) { + if (err.name !== "NS_ERROR_FAILURE") { + emitTestFail( + `${DummyUIService.testName}: Got unexpected '${err.name}' when changin shippingOption.` + ); + } else { + emitTestPass( + `${DummyUIService.testName}: Got expected 'NS_ERROR_FAILURE' when changing shippingOption.` + ); + } + } + + try { + paymentSrv.changeShippingOption(DummyUIService.requestId, billingAddress); + emitTestFail( + `${DummyUIService.testName}: Got unexpected success when changing shippingAddress.` + ); + } catch (err) { + if (err.name !== "NS_ERROR_FAILURE") { + emitTestFail( + `${DummyUIService.testName}: Got unexpected '${err.name}' when changing shippingAddress.` + ); + } else { + emitTestPass( + `${DummyUIService.testName}: Got expected 'NS_ERROR_FAILURE' when changing shippingAddress.` + ); + } + } + sendAsyncMessage("interact-with-payment-complete"); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/ClosePaymentChromeScript.js b/dom/payments/test/ClosePaymentChromeScript.js new file mode 100644 index 0000000000..60433f8f11 --- /dev/null +++ b/dom/payments/test/ClosePaymentChromeScript.js @@ -0,0 +1,160 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", `${DummyUIService.testName}: ${message}`); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", `${DummyUIService.testName}: ${message}`); +} + +addMessageListener("close-check", function () { + const paymentEnum = paymentSrv.enumerate(); + if (paymentEnum.hasMoreElements()) { + emitTestFail("Non-empty PaymentRequest queue in PaymentRequestService."); + } else { + emitTestPass("Got empty PaymentRequest queue in PaymentRequestService."); + } + sendAsyncMessage("close-check-complete"); +}); + +var setPaymentNums = 0; + +addMessageListener("payment-num-set", function () { + setPaymentNums = 0; + const paymentEnum = paymentSrv.enumerate(); + while (paymentEnum.hasMoreElements()) { + setPaymentNums = setPaymentNums + 1; + paymentEnum.getNext(); + } + sendAsyncMessage("payment-num-set-complete"); +}); + +addMessageListener("payment-num-check", function (expectedNumPayments) { + const paymentEnum = paymentSrv.enumerate(); + let numPayments = 0; + while (paymentEnum.hasMoreElements()) { + numPayments = numPayments + 1; + paymentEnum.getNext(); + } + if (numPayments !== expectedNumPayments + setPaymentNums) { + emitTestFail( + "Expected '" + + expectedNumPayments + + "' PaymentRequests in PaymentRequestService" + + ", but got '" + + numPayments + + "'." + ); + } else { + emitTestPass( + "Got expected '" + + numPayments + + "' PaymentRequests in PaymentRequestService." + ); + } + // force cleanup PaymentRequests for clear environment to next testcase. + paymentSrv.cleanup(); + sendAsyncMessage("payment-num-check-complete"); +}); + +addMessageListener("test-setup", testName => { + DummyUIService.testName = testName; + sendAsyncMessage("test-setup-complete"); +}); + +addMessageListener("reject-payment", expectedError => { + try { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + DummyUIService.respondRequestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + emitTestPass("Reject PaymentRequest successfully"); + } catch (error) { + if (expectedError) { + if (error.name === "NS_ERROR_FAILURE") { + emitTestPass( + "Got expected NS_ERROR_FAILURE when responding a closed PaymentRequest" + ); + sendAsyncMessage("reject-payment-complete"); + return; + } + } + emitTestFail( + "Unexpected error '" + + error.name + + "' when reponding a closed PaymentRequest" + ); + } + sendAsyncMessage("reject-payment-complete"); +}); + +addMessageListener("update-payment", () => { + try { + paymentSrv.changeShippingOption(DummyUIService.respondRequestId, ""); + emitTestPass("Change shippingOption succefully"); + } catch (error) { + emitTestFail( + "Unexpected error '" + error.name + "' when changing the shipping option" + ); + } + sendAsyncMessage("update-payment-complete"); +}); + +const DummyUIService = { + testName: "", + respondRequestId: "", + showPayment: requestId => { + DummyUIService.respondRequestId = requestId; + }, + abortPayment: requestId => { + DummyUIService.respondRequestId = requestId; + }, + completePayment: requestId => { + DummyUIService.respondRequestId = requestId; + }, + updatePayment: requestId => { + DummyUIService.respondRequestId = requestId; + }, + closePayment: requestId => { + this.respondRequestId = requestId; + }, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/ConstructorChromeScript.js b/dom/payments/test/ConstructorChromeScript.js new file mode 100644 index 0000000000..17e59f6241 --- /dev/null +++ b/dom/payments/test/ConstructorChromeScript.js @@ -0,0 +1,490 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} + +function checkSimplestRequest(payRequest) { + if (payRequest.topLevelPrincipal.origin != "https://example.com") { + emitTestFail( + "Top level principal's Origin should be 'https://example.com', but got '" + + payRequest.topLevelPrincipal.origin + + "'." + ); + } + + if (payRequest.paymentMethods.length != 1) { + emitTestFail("paymentMethods' length should be 1."); + } + + const methodData = payRequest.paymentMethods.queryElementAt( + 0, + Ci.nsIPaymentMethodData + ); + if (!methodData) { + emitTestFail("Fail to get payment methodData."); + } + const supportedMethod = methodData.supportedMethods; + if (supportedMethod != "basic-card") { + emitTestFail("supported method should be 'basic-card'."); + } + if (methodData.data) { + emitTestFail("methodData.data should not exist."); + } + + // checking the passed PaymentDetails parameter + const details = payRequest.paymentDetails; + if (details.totalItem.label != "Total") { + emitTestFail("total item's label should be 'Total'."); + } + if (details.totalItem.amount.currency != "USD") { + emitTestFail("total item's currency should be 'USD'."); + } + if (details.totalItem.amount.value != "1.00") { + emitTestFail("total item's value should be '1.00'."); + } + + if (details.displayItems.length !== 0) { + emitTestFail("details.displayItems should be an empty array."); + } + if (details.modifiers.length !== 0) { + emitTestFail("details.modifiers should be an empty array."); + } + if (details.shippingOptions.length !== 0) { + emitTestFail("details.shippingOptions should be an empty array."); + } + + // checking the default generated PaymentOptions parameter + const paymentOptions = payRequest.paymentOptions; + if (paymentOptions.requestPayerName) { + emitTestFail("requestPayerName option should be false."); + } + if (paymentOptions.requestPayerEmail) { + emitTestFail("requestPayerEmail option should be false."); + } + if (paymentOptions.requestPayerPhone) { + emitTestFail("requestPayerPhone option should be false."); + } + if (paymentOptions.requestShipping) { + emitTestFail("requestShipping option should be false."); + } + if (paymentOptions.shippingType != "shipping") { + emitTestFail("shippingType option should be 'shipping'."); + } +} + +// eslint-disable-next-line complexity +function checkComplexRequest(payRequest) { + if (payRequest.topLevelPrincipal.origin != "https://example.com") { + emitTestFail( + "Top level principal's origin should be 'https://example.com', but got '" + + payRequest.topLevelPrincipal.origin + + "'." + ); + } + + if (payRequest.paymentMethods.length != 1) { + emitTestFail("paymentMethods' length should be 1."); + } + + const methodData = payRequest.paymentMethods.queryElementAt( + 0, + Ci.nsIPaymentMethodData + ); + if (!methodData) { + emitTestFail("Fail to get payment methodData."); + } + let supportedMethod = methodData.supportedMethods; + if (supportedMethod != "basic-card") { + emitTestFail("supported method should be 'basic-card'."); + } + const data = methodData.data; + const supportedNetworks = data.supportedNetworks; + const expectedSupportedNetworks = [ + "unionpay", + "visa", + "mastercard", + "amex", + "discover", + "diners", + "jcb", + "mir", + ]; + if (supportedNetworks.length != expectedSupportedNetworks.length) { + emitTestFail( + "supportedNetworks.length should be " + + expectedSupportedNetworks.length + + ", but got " + + supportedNetworks.length + + "." + ); + } + for (let idx = 0; idx < supportedNetworks.length; idx++) { + if (supportedNetworks[idx] != expectedSupportedNetworks[idx]) { + emitTestFail( + "supportedNetworks[" + + idx + + "] should be '" + + expectedSupportedNetworks[idx] + + "', but got '" + + supportedNetworks[idx] + + "'." + ); + } + } + // checking the passed PaymentDetails parameter + const details = payRequest.paymentDetails; + if (details.id != "payment details") { + emitTestFail("details.id should be 'payment details'."); + } + if (details.totalItem.label != "Total") { + emitTestFail("total item's label should be 'Total'."); + } + if (details.totalItem.amount.currency != "USD") { + emitTestFail("total item's currency should be 'USD'."); + } + if (details.totalItem.amount.value != "100.00") { + emitTestFail("total item's value should be '100.00'."); + } + + const displayItems = details.displayItems; + if (!details.displayItems) { + emitTestFail("details.displayItems should not be undefined."); + } + if (displayItems.length != 2) { + emitTestFail("displayItems' length should be 2."); + } + let item = displayItems.queryElementAt(0, Ci.nsIPaymentItem); + if (item.label != "First item") { + emitTestFail("1st display item's label should be 'First item'."); + } + if (item.amount.currency != "USD") { + emitTestFail("1st display item's currency should be 'USD'."); + } + if (item.amount.value != "60.00") { + emitTestFail("1st display item's value should be '60.00'."); + } + item = displayItems.queryElementAt(1, Ci.nsIPaymentItem); + if (item.label != "Second item") { + emitTestFail("2nd display item's label should be 'Second item'."); + } + if (item.amount.currency != "USD") { + emitTestFail("2nd display item's currency should be 'USD'."); + } + if (item.amount.value != "40.00") { + emitTestFail("2nd display item's value should be '40.00'."); + } + + const modifiers = details.modifiers; + if (!modifiers) { + emitTestFail("details.displayItems should not be undefined."); + } + if (modifiers.length != 1) { + emitTestFail("modifiers' length should be 1."); + } + const modifier = modifiers.queryElementAt(0, Ci.nsIPaymentDetailsModifier); + const supportedMethods = modifier.supportedMethods; + if (supportedMethod != "basic-card") { + emitTestFail("modifier's supported method name should be 'basic-card'."); + } + if (modifier.total.label != "Discounted Total") { + emitTestFail("modifier's total label should be 'Discounted Total'."); + } + if (modifier.total.amount.currency != "USD") { + emitTestFail("modifier's total currency should be 'USD'."); + } + if (modifier.total.amount.value != "90.00") { + emitTestFail("modifier's total value should be '90.00'."); + } + + const additionalItems = modifier.additionalDisplayItems; + if (additionalItems.length != 1) { + emitTestFail("additionalDisplayItems' length should be 1."); + } + const additionalItem = additionalItems.queryElementAt(0, Ci.nsIPaymentItem); + if (additionalItem.label != "basic-card discount") { + emitTestFail("additional item's label should be 'basic-card discount'."); + } + if (additionalItem.amount.currency != "USD") { + emitTestFail("additional item's currency should be 'USD'."); + } + if (additionalItem.amount.value != "-10.00") { + emitTestFail("additional item's value should be '-10.00'."); + } + if (modifier.data.discountProgramParticipantId != "86328764873265") { + emitTestFail( + "modifier's data should be '86328764873265', but got '" + + modifier.data.discountProgramParticipantId + + "'." + ); + } + + const shippingOptions = details.shippingOptions; + if (!shippingOptions) { + emitTestFail("details.shippingOptions should not be undefined."); + } + if (shippingOptions.length != 2) { + emitTestFail("shippingOptions' length should be 2."); + } + let shippingOption = shippingOptions.queryElementAt( + 0, + Ci.nsIPaymentShippingOption + ); + if (shippingOption.id != "NormalShipping") { + emitTestFail("1st shippingOption's id should be 'NormalShipping'."); + } + if (shippingOption.label != "NormalShipping") { + emitTestFail("1st shippingOption's lable should be 'NormalShipping'."); + } + if (shippingOption.amount.currency != "USD") { + emitTestFail("1st shippingOption's amount currency should be 'USD'."); + } + if (shippingOption.amount.value != "10.00") { + emitTestFail("1st shippingOption's amount value should be '10.00'."); + } + if (!shippingOption.selected) { + emitTestFail("1st shippingOption should be selected."); + } + shippingOption = shippingOptions.queryElementAt( + 1, + Ci.nsIPaymentShippingOption + ); + if (shippingOption.id != "FastShipping") { + emitTestFail("2nd shippingOption's id should be 'FastShipping'."); + } + if (shippingOption.label != "FastShipping") { + emitTestFail("2nd shippingOption's lable should be 'FastShipping'."); + } + if (shippingOption.amount.currency != "USD") { + emitTestFail("2nd shippingOption's amount currency should be 'USD'."); + } + if (shippingOption.amount.value != "30.00") { + emitTestFail("2nd shippingOption's amount value should be '30.00'."); + } + if (shippingOption.selected) { + emitTestFail("2nd shippingOption should not be selected."); + } + + // checking the default generated PaymentOptions parameter + const paymentOptions = payRequest.paymentOptions; + if (!paymentOptions.requestPayerName) { + emitTestFail("requestPayerName option should be true."); + } + if (!paymentOptions.requestPayerEmail) { + emitTestFail("requestPayerEmail option should be true."); + } + if (!paymentOptions.requestPayerPhone) { + emitTestFail("requestPayerPhone option should be true."); + } + if (!paymentOptions.requestShipping) { + emitTestFail("requestShipping option should be true."); + } + if (paymentOptions.shippingType != "shipping") { + emitTestFail("shippingType option should be 'shipping'."); + } +} + +function checkNonBasicCardRequest(payRequest) { + if (payRequest.paymentMethods.length != 1) { + emitTestFail("paymentMethods' length should be 1."); + } + + const methodData = payRequest.paymentMethods.queryElementAt( + 0, + Ci.nsIPaymentMethodData + ); + if (!methodData) { + emitTestFail("Fail to get payment methodData."); + } + const supportedMethod = methodData.supportedMethods; + if (supportedMethod != "testing-payment-method") { + emitTestFail("supported method should be 'testing-payment-method'."); + } + + const paymentId = methodData.data.paymentId; + if (paymentId != "P3892940") { + emitTestFail( + "methodData.data.paymentId should be 'P3892940', but got " + + paymentId + + "." + ); + } + const paymentType = methodData.data.paymentType; + if (paymentType != "prepaid") { + emitTestFail( + "methodData.data.paymentType should be 'prepaid', but got " + + paymentType + + "." + ); + } + + // checking the passed PaymentDetails parameter + const details = payRequest.paymentDetails; + if (details.totalItem.label != "Total") { + emitTestFail("total item's label should be 'Total'."); + } + if (details.totalItem.amount.currency != "USD") { + emitTestFail("total item's currency should be 'USD'."); + } + if (details.totalItem.amount.value != "1.00") { + emitTestFail("total item's value should be '1.00'."); + } + + if (details.displayItems.length !== 0) { + emitTestFail("details.displayItems should be an zero length array."); + } + if (details.displayItems.length !== 0) { + emitTestFail("details.modifiers should be an zero length array."); + } + if (details.displayItems.length !== 0) { + emitTestFail("details.shippingOptions should be an zero length array."); + } + + // checking the default generated PaymentOptions parameter + const paymentOptions = payRequest.paymentOptions; + if (paymentOptions.requestPayerName) { + emitTestFail("requestPayerName option should be false."); + } + if (paymentOptions.requestPayerEmail) { + emitTestFail("requestPayerEmail option should be false."); + } + if (paymentOptions.requestPayerPhone) { + emitTestFail("requestPayerPhone option should be false."); + } + if (paymentOptions.requestShipping) { + emitTestFail("requestShipping option should be false."); + } + if (paymentOptions.shippingType != "shipping") { + emitTestFail("shippingType option should be 'shipping'."); + } +} + +function checkSimplestRequestHandler() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + emitTestFail( + "PaymentRequestService should have at least one payment request." + ); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + emitTestFail("Fail to get existing payment request."); + break; + } + checkSimplestRequest(payRequest); + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +function checkComplexRequestHandler() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + emitTestFail( + "PaymentRequestService should have at least one payment request." + ); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + emitTestFail("Fail to get existing payment request."); + break; + } + checkComplexRequest(payRequest); + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +function checkNonBasicCardRequestHandler() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + emitTestFail( + "PaymentRequestService should have at least one payment request." + ); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + emitTestFail("Fail to get existing payment request."); + break; + } + checkNonBasicCardRequest(payRequest); + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +function checkMultipleRequestsHandler() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + emitTestFail( + "PaymentRequestService should have at least one payment request." + ); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + emitTestFail("Fail to get existing payment request."); + break; + } + if (payRequest.paymentDetails.id == "payment details") { + checkComplexRequest(payRequest); + } else { + checkSimplestRequest(payRequest); + } + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +function checkCrossOriginTopLevelPrincipalHandler() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + emitTestFail( + "PaymentRequestService should have at least one payment request." + ); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + emitTestFail("Fail to get existing payment request."); + break; + } + if (payRequest.topLevelPrincipal.origin != "https://example.com") { + emitTestFail( + "Top level principal's origin should be 'https://example.com', but got '" + + payRequest.topLevelPrincipal.origin + + "'." + ); + } + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +addMessageListener("check-simplest-request", checkSimplestRequestHandler); +addMessageListener("check-complex-request", checkComplexRequestHandler); +addMessageListener("check-multiple-requests", checkMultipleRequestsHandler); +addMessageListener( + "check-nonbasiccard-request", + checkNonBasicCardRequestHandler +); +addMessageListener( + "check-cross-origin-top-level-principal", + checkCrossOriginTopLevelPrincipalHandler +); + +addMessageListener("teardown", function () { + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/CurrencyAmountValidationChromeScript.js b/dom/payments/test/CurrencyAmountValidationChromeScript.js new file mode 100644 index 0000000000..a15e79be18 --- /dev/null +++ b/dom/payments/test/CurrencyAmountValidationChromeScript.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +const InvalidDetailsUIService = { + showPayment(requestId) { + paymentSrv.changeShippingOption(requestId, ""); + }, + abortPayment(requestId) { + const abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED); + paymentSrv.respondPayment( + abortResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + completePayment(requestId) {}, + updatePayment(requestId) {}, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +function checkLowerCaseCurrency() { + const paymentEnum = paymentSrv.enumerate(); + if (!paymentEnum.hasMoreElements()) { + const msg = + "PaymentRequestService should have at least one payment request."; + sendAsyncMessage("test-fail", msg); + } + for (let payRequest of paymentEnum) { + if (!payRequest) { + sendAsyncMessage("test-fail", "Fail to get existing payment request."); + break; + } + const { currency } = payRequest.paymentDetails.totalItem.amount; + if (currency != "USD") { + const msg = + "Currency of PaymentItem total should be 'USD', but got ${currency}"; + sendAsyncMessage("check-complete"); + } + } + paymentSrv.cleanup(); + sendAsyncMessage("check-complete"); +} + +addMessageListener("check-lower-case-currency", checkLowerCaseCurrency); + +addMessageListener("set-update-with-invalid-details-ui-service", () => { + paymentSrv.setTestingUIService( + InvalidDetailsUIService.QueryInterface(Ci.nsIPaymentUIService) + ); +}); + +addMessageListener("teardown", () => sendAsyncMessage("teardown-complete")); diff --git a/dom/payments/test/DefaultData.js b/dom/payments/test/DefaultData.js new file mode 100644 index 0000000000..13723b5799 --- /dev/null +++ b/dom/payments/test/DefaultData.js @@ -0,0 +1,59 @@ +// testing data declation +const defaultMethods = [ + { + supportedMethods: "basic-card", + data: { + supportedNetworks: [ + "unionpay", + "visa", + "mastercard", + "amex", + "discover", + "diners", + "jcb", + "mir", + ], + }, + }, + { + supportedMethods: "testing-payment-method", + }, +]; + +const defaultDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00", + }, + selected: false, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "5.00", + }, + selected: false, + }, + ], +}; + +const defaultOptions = { + requestPayerName: true, + requestPayerEmail: false, + requestPayerPhone: false, + requestShipping: true, + shippingType: "shipping", +}; diff --git a/dom/payments/test/GeneralChromeScript.js b/dom/payments/test/GeneralChromeScript.js new file mode 100644 index 0000000000..74fe4c299a --- /dev/null +++ b/dom/payments/test/GeneralChromeScript.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/PMIValidationChromeScript.js b/dom/payments/test/PMIValidationChromeScript.js new file mode 100644 index 0000000000..3deec2a89c --- /dev/null +++ b/dom/payments/test/PMIValidationChromeScript.js @@ -0,0 +1,82 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +const UIService = { + showPayment(requestId) { + paymentSrv.changeShippingOption(requestId, ""); + }, + abortPayment(requestId) { + let abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED); + paymentSrv.respondPayment( + abortResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + completePayment(requestId) { + const completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + updatePayment(requestId) { + const showResponseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + showResponseData.initData({ + paymentToken: "6880281f-0df3-4b8e-916f-66575e2457c1", + }); + + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "https://example.com", // payment method + showResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} + +addMessageListener("set-ui-service", function () { + paymentSrv.setTestingUIService( + UIService.QueryInterface(Ci.nsIPaymentUIService) + ); +}); + +addMessageListener("teardown", function () { + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/PayerDetailsChromeScript.js b/dom/payments/test/PayerDetailsChromeScript.js new file mode 100644 index 0000000000..77024cc754 --- /dev/null +++ b/dom/payments/test/PayerDetailsChromeScript.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +const TestingUIService = { + showPayment(requestId, name = "", email = "", phone = "") { + const showResponseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + showResponseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + showResponseData, // payment method data + name, + email, + phone + ); + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + // .retry({ payer }) and .updateWith({payerErrors}) both get routed here: + updatePayment(requestId) { + // Let's echo what was sent in by the error... + const request = paymentSrv.getPaymentRequestById(requestId); + const { name, email, phone } = request.paymentDetails.payerErrors; + const { error } = request.paymentDetails; + // Let's use the .error as the switch + switch (error) { + case "retry-fire-payerdetaichangeevent": { + paymentSrv.changePayerDetail(requestId, name, email, phone); + break; + } + case "update-with": { + this.showPayment(requestId, name, email, phone); + break; + } + default: + const msg = `Expect details.error value: '${error}'`; + sendAsyncMessage("test-fail", msg); + } + }, + completePayment(requestId) { + const request = paymentSrv.getPaymentRequestById(requestId); + const completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + get QueryInterface() { + return ChromeUtils.generateQI(["nsIPaymentUIService"]); + }, +}; + +paymentSrv.setTestingUIService( + TestingUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("teardown", () => { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/RequestShippingChromeScript.js b/dom/payments/test/RequestShippingChromeScript.js new file mode 100644 index 0000000000..d8dafb8a08 --- /dev/null +++ b/dom/payments/test/RequestShippingChromeScript.js @@ -0,0 +1,116 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} + +const shippingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +shippingAddress.init( + "", // country + addressLine, // address line + "", // region + "", // region code + "", // city + "", // dependent locality + "", // postal code + "", // sorting code + "", // organization + "", // recipient + "" +); // phone + +const NormalUIService = { + shippingOptionChanged: false, + showPayment(requestId) { + paymentSrv.changeShippingAddress(requestId, shippingAddress); + }, + abortPayment(requestId) {}, + completePayment(requestId) { + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + updatePayment(requestId) { + let showResponse = null; + let payRequest = paymentSrv.getPaymentRequestById(requestId); + + const shippingOptions = payRequest.paymentDetails.shippingOptions; + if (shippingOptions.length) { + emitTestFail("Wrong length for shippingOptions."); + } + + const showResponseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + + try { + showResponseData.initData({ + paymentToken: "6880281f-0df3-4b8e-916f-66575e2457c1", + }); + } catch (e) { + emitTestFail( + 'Fail to initialize response data with { paymentToken: "6880281f-0df3-4b8e-916f-66575e2457c1",}' + ); + } + + showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + showResponseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +addMessageListener("set-normal-ui-service", function () { + paymentSrv.setTestingUIService( + NormalUIService.QueryInterface(Ci.nsIPaymentUIService) + ); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/RetryPaymentChromeScript.js b/dom/payments/test/RetryPaymentChromeScript.js new file mode 100644 index 0000000000..d1486d676d --- /dev/null +++ b/dom/payments/test/RetryPaymentChromeScript.js @@ -0,0 +1,238 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", message); +} + +const billingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +billingAddress.init( + "USA", // country + addressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "", // dependent locality + "94066", // postal code + "123456", // sorting code + "", // organization + "Bill A. Pacheco", // recipient + "+14344413879" +); // phone + +function acceptPayment(requestId, mode) { + const basiccardResponseData = Cc[ + "@mozilla.org/dom/payments/basiccard-response-data;1" + ].createInstance(Ci.nsIBasicCardResponseData); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + basiccardResponseData.initData( + "Bill A. Pacheco", // cardholderName + "4916855166538720", // cardNumber + "01", // expiryMonth + "2024", // expiryYear + "180", // cardSecurityCode + billingAddress + ); // billingAddress + if (mode === "show") { + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + basiccardResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + } + if (mode == "retry") { + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "basic-card", // payment method + basiccardResponseData, // payment method data + "Bill A. Pacheco", // payer name + "bpacheco@test.org", // payer email + "+123456789" + ); // payer phone + } + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function rejectPayment(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function checkAddressErrors(testName, errors) { + if (!errors) { + emitTestFail( + `${testName}: Expect non-null shippingAddressErrors, but got null.` + ); + return; + } + for (const [key, msg] of Object.entries(errors)) { + const expected = `${key} error`; + if (msg !== expected) { + emitTestFail( + `${testName}: Expected '${expected}' on shippingAddressErrors.${key}, but got '${msg}'.` + ); + return; + } + } +} + +function checkPayerErrors(testName, errors) { + if (!errors) { + emitTestFail(`${testName}: Expect non-null payerErrors, but got null.`); + return; + } + for (const [key, msg] of Object.entries(errors)) { + const expected = `${key} error`; + if (msg !== expected) { + emitTestFail( + `${testName}: Expected '${expected}' on payerErrors.${key}, but got '${msg}'.` + ); + return; + } + } +} + +function checkPaymentMethodErrors(testName, errors) { + if (!errors) { + emitTestFail( + `${testName} :Expect non-null paymentMethodErrors, but got null.` + ); + return; + } + for (const [key, msg] of Object.entries(errors)) { + const expected = `method ${key} error`; + if (msg !== expected) { + emitTestFail( + `${testName}: Expected '${expected}' on paymentMethodErrors.${key}, but got '${msg}'.` + ); + return; + } + } +} + +const DummyUIService = { + testName: "", + rejectRetry: false, + showPayment(requestId) { + acceptPayment(requestId, "show"); + }, + abortPaymen(requestId) { + respondRequestId = requestId; + }, + completePayment(requestId) { + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + updatePayment(requestId) { + const payment = paymentSrv.getPaymentRequestById(requestId); + if (payment.paymentDetails.error !== "error") { + emitTestFail( + "Expect 'error' on details.error, but got '" + + payment.paymentDetails.error + + "'" + ); + } + checkAddressErrors( + this.testName, + payment.paymentDetails.shippingAddressErrors + ); + checkPayerErrors(this.testName, payment.paymentDetails.payerErrors); + checkPaymentMethodErrors( + this.testName, + payment.paymentDetails.paymentMethodErrors + ); + if (this.rejectRetry) { + rejectPayment(requestId); + } else { + acceptPayment(requestId, "retry"); + } + }, + closePayment: requestId => { + respondRequestId = requestId; + }, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("start-test", function (testName) { + DummyUIService.testName = testName; + sendAsyncMessage("start-test-complete"); +}); + +addMessageListener("finish-test", function () { + DummyUIService.testName = ""; + sendAsyncMessage("finish-test-complete"); +}); + +addMessageListener("reject-retry", function () { + DummyUIService.rejectRetry = true; + sendAsyncMessage("reject-retry-complete"); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/ShippingOptionsChromeScript.js b/dom/payments/test/ShippingOptionsChromeScript.js new file mode 100644 index 0000000000..d3f23ab391 --- /dev/null +++ b/dom/payments/test/ShippingOptionsChromeScript.js @@ -0,0 +1,120 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", message); +} + +let expectedRequestOption = null; +let expectedUpdatedOption = null; +let changeShippingOption = null; + +function showResponse(requestId) { + const showResponseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + showResponseData.initData({}); + const showActionResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showActionResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + showResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showActionResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function showRequest(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + const message = + "request.shippingOption should be " + + expectedRequestOption + + " when calling show(), but got " + + request.shippingOption + + "."; + if (request.shippingOption != expectedRequestOption) { + emitTestFail(message); + } else { + emitTestPass(message); + } + if (changeShippingOption) { + paymentSrv.changeShippingOption(requestId, changeShippingOption); + } else { + showResponse(requestId); + } +} + +function updateRequest(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + const message = + "request.shippingOption should be " + + expectedUpdatedOption + + " when calling updateWith(), but got " + + request.shippingOption + + "."; + if (request.shippingOption != expectedUpdatedOption) { + emitTestFail(message); + } else { + emitTestPass(message); + } + showResponse(requestId); +} + +const TestingUIService = { + showPayment: showRequest, + abortPayment(requestId) {}, + completePayment(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + }, + updatePayment: updateRequest, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + TestingUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("set-expected-results", function (results) { + expectedRequestOption = results.requestResult; + expectedUpdatedOption = results.responseResult; + changeShippingOption = results.changeOptionResult; +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/ShowPaymentChromeScript.js b/dom/payments/test/ShowPaymentChromeScript.js new file mode 100644 index 0000000000..c8848ab622 --- /dev/null +++ b/dom/payments/test/ShowPaymentChromeScript.js @@ -0,0 +1,393 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", `${DummyUIService.testName}: ${message}`); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", `${DummyUIService.testName}: ${message}`); +} + +const shippingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +shippingAddress.init( + "USA", // country + addressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "Test locality", // dependent locality + "94066", // postal code + "123456", // sorting code + "Testing Org", // organization + "Bill A. Pacheco", // recipient + "+1-434-441-3879" +); // phone + +function acceptShow(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({ + paymentToken: "6880281f-0df3-4b8e-916f-66575e2457c1", + }); + let showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + responseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function rejectShow(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function updateShow(requestId) { + if (DummyUIService.expectedUpdateAction == "updateaddress") { + paymentSrv.changeShippingAddress(requestId, shippingAddress); + } else if ( + DummyUIService.expectedUpdateAction == "accept" || + DummyUIService.expectedUpdateAction == "error" + ) { + paymentSrv.changeShippingOption(requestId, "FastShipping"); + } else { + emitTestFail( + "Unknown expected update action: " + DummyUIService.expectedUpdateAction + ); + } +} + +function showRequest(requestId) { + const request = paymentSrv.getPaymentRequestById(requestId); + if (request.completeStatus == "initial") { + return; + } + if (DummyUIService.expectedShowAction == "accept") { + acceptShow(requestId); + } else if (DummyUIService.expectedShowAction == "reject") { + rejectShow(requestId); + } else if (DummyUIService.expectedShowAction == "update") { + updateShow(requestId); + } else { + emitTestFail( + "Unknown expected show action: " + DummyUIService.expectedShowAction + ); + } +} + +function abortRequest(requestId) { + let abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED); + paymentSrv.respondPayment(abortResponse); +} + +function completeRequest(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + if (DummyUIService.expectedCompleteStatus) { + if (request.completeStatus == DummyUIService.expectedCompleteStatus) { + emitTestPass( + "request.completeStatus matches expectation of " + + DummyUIService.expectedCompleteStatus + ); + } else { + emitTestFail( + "request.completeStatus incorrect. Expected " + + DummyUIService.expectedCompleteStatus + + ", got " + + request.completeStatus + ); + } + } + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function updateRequest(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + if (request.completeStatus !== "") { + emitTestFail( + "request.completeStatus should be empty, but got '" + + request.completeStatus + + "'." + ); + } + if (DummyUIService.expectedUpdateAction == "accept") { + if (request.paymentDetails.error != "") { + emitTestFail( + "updatedDetails should not have errors(" + + request.paymentDetails.error + + ")." + ); + } + const shippingOptions = request.paymentDetails.shippingOptions; + let shippingOption = shippingOptions.queryElementAt( + 0, + Ci.nsIPaymentShippingOption + ); + if (shippingOption.selected) { + emitTestFail(shippingOption.label + " should not be selected."); + } + shippingOption = shippingOptions.queryElementAt( + 1, + Ci.nsIPaymentShippingOption + ); + if (!shippingOption.selected) { + emitTestFail(shippingOption.label + " should be selected."); + } + acceptShow(requestId); + } else if (DummyUIService.expectedUpdateAction == "error") { + if (request.paymentDetails.error != "Update with Error") { + emitTestFail( + "details.error should be 'Update with Error', but got " + + request.paymentDetails.error + + "." + ); + } + rejectShow(requestId); + } else if (DummyUIService.expectedUpdateAction == "updateaddress") { + if (request.paymentDetails.error != "") { + emitTestFail( + "updatedDetails should not have errors(" + + request.paymentDetails.error + + ")." + ); + } + DummyUIService.expectedUpdateAction = "accept"; + paymentSrv.changeShippingOption(requestId, "FastShipping"); + } else { + emitTestFail( + "Unknown expected update aciton: " + DummyUIService.expectedUpdateAction + ); + } +} + +const DummyUIService = { + testName: "", + expectedCompleteStatus: null, + expectedShowAction: "accept", + expectedUpdateAction: "accept", + showPayment: showRequest, + abortPayment: abortRequest, + completePayment: completeRequest, + updatePayment: updateRequest, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +function testShowResponseInit() { + const showResponseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + try { + showResponseData.initData(null); + emitTestFail( + "nsIGeneralResponseData can not be initialized with null object." + ); + } catch (e) { + if (e.name != "NS_ERROR_FAILURE") { + emitTestFail( + "Expected 'NS_ERROR_FAILURE' when initializing nsIGeneralResponseData with null object, but got " + + e.name + + "." + ); + } + emitTestPass( + "Get expected result for initializing nsIGeneralResponseData with null object" + ); + } + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + try { + showResponse.init( + "test request id", + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + showResponseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + emitTestPass( + "Get expected result for initializing response with accepted and empty data." + ); + } catch (e) { + emitTestFail( + "Unexpected error " + + e.name + + " when initializing response with accepted and empty data." + ); + } + + try { + showResponse.init( + "test request id", + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "testing-payment-method", + null, + "Bill A. Pacheco", + "", + "" + ); + emitTestPass( + "Get expected result for initializing response with rejected and null data." + ); + } catch (e) { + emitTestFail( + "Unexpected error " + + e.name + + " when initializing response with rejected and null data." + ); + } + + try { + showResponse.init( + "test request id", + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", + null, + "Bill A. Pacheco", + "", + "" + ); + emitTestFail( + "nsIPaymentShowActionResponse can not be initialized with accpeted and null data." + ); + } catch (e) { + if (e.name != "NS_ERROR_ILLEGAL_VALUE") { + emitTestFail( + "Expected 'NS_ERROR_ILLEGAL_VALUE', but got " + e.name + "." + ); + } + emitTestPass( + "Get expected result for initializing response with accepted and null data." + ); + } + sendAsyncMessage("test-show-response-init-complete"); +} + +addMessageListener("set-simple-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.expectedCompleteStatus = null; + DummyUIService.expectedShowAction = "accept"; + DummyUIService.expectedUpdateAction = "accept"; + sendAsyncMessage("set-simple-ui-service-complete"); +}); + +addMessageListener("set-normal-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.expectedCompleteStatus = null; + DummyUIService.expectedShowAction = "update"; + DummyUIService.expectedUpdateAction = "updateaddress"; + sendAsyncMessage("set-normal-ui-service-complete"); +}); + +addMessageListener("set-reject-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.expectedCompleteStatus = null; + DummyUIService.expectedShowAction = "reject"; + DummyUIService.expectedUpdateAction = "error"; + sendAsyncMessage("set-reject-ui-service-complete"); +}); + +addMessageListener("set-update-with-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.expectedCompleteStatus = null; + DummyUIService.expectedShowAction = "update"; + DummyUIService.expectedUpdateAction = "accept"; + sendAsyncMessage("set-update-with-ui-service-complete"); +}); + +addMessageListener("set-update-with-error-ui-service", function (testName) { + DummyUIService.testName = testName; + DummyUIService.expectedCompleteStatus = null; + DummyUIService.expectedShowAction = "update"; + DummyUIService.expectedUpdateAction = "error"; + sendAsyncMessage("set-update-with-error-ui-service-complete"); +}); + +addMessageListener("test-show-response-init", testShowResponseInit); + +addMessageListener("set-complete-status-success", function () { + DummyUIService.expectedCompleteStatus = "success"; + sendAsyncMessage("set-complete-status-success-complete"); +}); + +addMessageListener("set-complete-status-fail", function () { + DummyUIService.expectedCompleteStatus = "fail"; + sendAsyncMessage("set-complete-status-fail-complete"); +}); + +addMessageListener("set-complete-status-unknown", function () { + DummyUIService.expectedCompleteStatus = "unknown"; + sendAsyncMessage("set-complete-status-unknown-complete"); +}); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/UpdateErrorsChromeScript.js b/dom/payments/test/UpdateErrorsChromeScript.js new file mode 100644 index 0000000000..ac288dbc15 --- /dev/null +++ b/dom/payments/test/UpdateErrorsChromeScript.js @@ -0,0 +1,214 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" +].getService(Ci.nsIPaymentRequestService); + +function emitTestFail(message) { + sendAsyncMessage("test-fail", message); +} +function emitTestPass(message) { + sendAsyncMessage("test-pass", message); +} + +const shippingAddress = Cc[ + "@mozilla.org/dom/payments/payment-address;1" +].createInstance(Ci.nsIPaymentAddress); +const addressLine = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray +); +const address = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString +); +address.data = "Easton Ave"; +addressLine.appendElement(address); +shippingAddress.init( + "USA", // country + addressLine, // address line + "CA", // region + "CA", // region code + "San Bruno", // city + "Test locality", // dependent locality + "94066", // postal code + "123456", // sorting code + "Testing Org", // organization + "Bill A. Pacheco", // recipient + "+1-434-441-3879" +); // phone + +function acceptShow(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({ + paymentToken: "6880281f-0df3-4b8e-916f-66575e2457c1", + }); + let showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, + "testing-payment-method", // payment method + responseData, // payment method data + "Bill A. Pacheco", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function rejectShow(requestId) { + const responseData = Cc[ + "@mozilla.org/dom/payments/general-response-data;1" + ].createInstance(Ci.nsIGeneralResponseData); + responseData.initData({}); + const showResponse = Cc[ + "@mozilla.org/dom/payments/payment-show-action-response;1" + ].createInstance(Ci.nsIPaymentShowActionResponse); + showResponse.init( + requestId, + Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, + "", // payment method + responseData, // payment method data + "", // payer name + "", // payer email + "" + ); // payer phone + paymentSrv.respondPayment( + showResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function updateShow(requestId) { + paymentSrv.changeShippingAddress(requestId, shippingAddress); +} + +function showRequest(requestId) { + updateShow(requestId); +} + +function abortRequest(requestId) { + let abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED); + paymentSrv.respondPayment(abortResponse); +} + +function completeRequest(requestId) { + let payRequest = paymentSrv.getPaymentRequestById(requestId); + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init( + requestId, + Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + ); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); +} + +function checkAddressErrors(errors) { + if (!errors) { + emitTestFail("Expect non-null shippingAddressErrors, but got null."); + } + if (errors.addressLine != "addressLine error") { + emitTestFail( + "Expect shippingAddressErrors.addressLine as 'addressLine error', but got" + + errors.addressLine + ); + } + if (errors.city != "city error") { + emitTestFail( + "Expect shippingAddressErrors.city as 'city error', but got" + errors.city + ); + } + if (errors.dependentLocality != "dependentLocality error") { + emitTestFail( + "Expect shippingAddressErrors.dependentLocality as 'dependentLocality error', but got" + + errors.dependentLocality + ); + } + if (errors.organization != "organization error") { + emitTestFail( + "Expect shippingAddressErrors.organization as 'organization error', but got" + + errors.organization + ); + } + if (errors.phone != "phone error") { + emitTestFail( + "Expect shippingAddressErrors.phone as 'phone error', but got" + + errors.phone + ); + } + if (errors.postalCode != "postalCode error") { + emitTestFail( + "Expect shippingAddressErrors.postalCode as 'postalCode error', but got" + + errors.postalCode + ); + } + if (errors.recipient != "recipient error") { + emitTestFail( + "Expect shippingAddressErrors.recipient as 'recipient error', but got" + + errors.recipient + ); + } + if (errors.region != "region error") { + emitTestFail( + "Expect shippingAddressErrors.region as 'region error', but got" + + errors.region + ); + } + if (errors.regionCode != "regionCode error") { + emitTestFail( + "Expect shippingAddressErrors.regionCode as 'regionCode error', but got" + + errors.region + ); + } + if (errors.sortingCode != "sortingCode error") { + emitTestFail( + "Expect shippingAddressErrors.sortingCode as 'sortingCode error', but got" + + errors.sortingCode + ); + } +} + +function updateRequest(requestId) { + let request = paymentSrv.getPaymentRequestById(requestId); + const addressErrors = request.paymentDetails.shippingAddressErrors; + const payerErrors = request.paymentDetails.payerErrors; + checkAddressErrors(addressErrors); + rejectShow(requestId); +} + +const DummyUIService = { + showPayment: showRequest, + abortPayment: abortRequest, + completePayment: completeRequest, + updatePayment: updateRequest, + closePayment(requestId) {}, + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), +}; + +paymentSrv.setTestingUIService( + DummyUIService.QueryInterface(Ci.nsIPaymentUIService) +); + +addMessageListener("teardown", function () { + paymentSrv.setTestingUIService(null); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/payments/test/blank_page.html b/dom/payments/test/blank_page.html new file mode 100644 index 0000000000..7323b00a28 --- /dev/null +++ b/dom/payments/test/blank_page.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Payment Request Testing</title> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + </head> + <body> + <h1>blank page.html</h1> + <script type="text/javascript"> + if(window.parent) { + window.parent.postMessage("successful", '*'); + } + </script> + </body> +</html> diff --git a/dom/payments/test/browser.ini b/dom/payments/test/browser.ini new file mode 100644 index 0000000000..1fb3809b80 --- /dev/null +++ b/dom/payments/test/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +prefs = + dom.payments.request.enabled=true +skip-if = + true # we don't ship webpayments right now bug 1514425 +support-files = + head.js + simple_payment_request.html + +[browser_payment_in_different_tabs.js] diff --git a/dom/payments/test/browser_payment_in_different_tabs.js b/dom/payments/test/browser_payment_in_different_tabs.js new file mode 100644 index 0000000000..c811d32dd2 --- /dev/null +++ b/dom/payments/test/browser_payment_in_different_tabs.js @@ -0,0 +1,37 @@ +"use strict"; + +// kTestRoot is from head.js +const kTestPage = kTestRoot + "simple_payment_request.html"; +const TABS_TO_OPEN = 5; +add_task(async () => { + Services.prefs.setBoolPref("dom.payments.request.enabled", true); + const tabs = []; + const options = { + gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser, + url: kTestPage, + }; + for (let i = 0; i < TABS_TO_OPEN; i++) { + const tab = await BrowserTestUtils.openNewForegroundTab(options); + tabs.push(tab); + } + const paymentSrv = Cc[ + "@mozilla.org/dom/payments/payment-request-service;1" + ].getService(Ci.nsIPaymentRequestService); + const paymentEnum = paymentSrv.enumerate(); + ok( + paymentEnum.hasMoreElements(), + "PaymentRequestService should have at least one payment request." + ); + const payments = new Set(); + for (let payment of paymentEnum) { + ok(payment, "Fail to get existing payment request."); + checkSimplePayment(payment); + payments.add(payment); + } + is(payments.size, TABS_TO_OPEN, `Should be ${TABS_TO_OPEN} unique objects.`); + for (const tab of tabs) { + await TestUtils.waitForTick(); + BrowserTestUtils.removeTab(tab); + } + Services.prefs.setBoolPref("dom.payments.request.enabled", false); +}); diff --git a/dom/payments/test/bug1478740.html b/dom/payments/test/bug1478740.html new file mode 100644 index 0000000000..ddcc04bbb0 --- /dev/null +++ b/dom/payments/test/bug1478740.html @@ -0,0 +1,44 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Payment Request Testing</title> +<script> +const methods = [ + { + supportedMethods: "basic-card", + }, +]; +const details = { + id: "simple details", + total: { + label: "Donation", + amount: { currency: "USD", value: "55.00" }, + }, +}; +const updatedDetails = { + id: "simple details", + total: { + label: "Donation", + amount: { currency: "USD", value: "55.00" }, + }, + error: "", +}; + +window.onmessage = async ({ data: action }) => { + let msg = "successful"; + switch (action) { + case "Show Payment": + try { + let request = new PaymentRequest(methods, details); + let responsePromise = await request.show(); + } catch (err) { + msg = err.name; + } + window.parent.postMessage(msg, "*") + break; + default: + window.parent.postMessage(`fail - unknown postmessage action: ${action}`, "*"); + } +}; + +window.parent.postMessage("successful", "*"); +</script> diff --git a/dom/payments/test/echo_payment_request.html b/dom/payments/test/echo_payment_request.html new file mode 100644 index 0000000000..b1bf3da90c --- /dev/null +++ b/dom/payments/test/echo_payment_request.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + <head> + <title>Payment Request Testing</title> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> + <script> + window.onmessage = (e) => { + const paymentArgs = [[{supportedMethods: 'basic-card'}], {total: {label: 'label', amount: {currency: 'USD', value: '5.00'}}}]; + + if (e.data === 'new PaymentRequest') { + try { + new PaymentRequest(...paymentArgs); + if (window.parent) { + window.parent.postMessage("successful", '*'); + } + } catch(ex) { + if (window.parent) { + window.parent.postMessage(ex.name, '*'); + } + } + } else if (e.data === 'new PaymentRequest in a new iframe') { + var ifrr = document.createElement('iframe'); + ifrr.allow = "payment"; + ifrr.src = "https://example.com/tests/dom/payments/test/simple_payment_request.html"; + document.body.appendChild(ifrr); + } else { + if (window.parent) { + window.parent.postMessage(e.data, '*'); + } + } + } + </script> +</body> +</html> diff --git a/dom/payments/test/head.js b/dom/payments/test/head.js new file mode 100644 index 0000000000..3a377e09ae --- /dev/null +++ b/dom/payments/test/head.js @@ -0,0 +1,127 @@ +const kTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +function checkSimplePayment(aSimplePayment) { + // checking the passed PaymentMethods parameter + is( + aSimplePayment.paymentMethods.length, + 1, + "paymentMethods' length should be 1." + ); + + const methodData = aSimplePayment.paymentMethods.queryElementAt( + 0, + Ci.nsIPaymentMethodData + ); + ok(methodData, "Fail to get payment methodData."); + is( + methodData.supportedMethods, + "basic-card", + "supported method should be 'basic-card'." + ); + ok(!methodData.data, "methodData.data should not exist."); + + // checking the passed PaymentDetails parameter + const details = aSimplePayment.paymentDetails; + is(details.id, "simple details", "details.id should be 'simple details'."); + is( + details.totalItem.label, + "Donation", + "total item's label should be 'Donation'." + ); + is( + details.totalItem.amount.currency, + "USD", + "total item's currency should be 'USD'." + ); + is( + details.totalItem.amount.value, + "55.00", + "total item's value should be '55.00'." + ); + + is( + details.displayItems.length, + 0, + "details.displayItems should be a zero length array." + ); + is( + details.modifiers.length, + 0, + "details.modifiers should be a zero length array." + ); + is( + details.shippingOptions.length, + 0, + "details.shippingOptions should be a zero length array." + ); + + // checking the default generated PaymentOptions parameter + const paymentOptions = aSimplePayment.paymentOptions; + ok(!paymentOptions.requestPayerName, "payerName option should be false"); + ok(!paymentOptions.requestPayerEmail, "payerEmail option should be false"); + ok(!paymentOptions.requestPayerPhone, "payerPhone option should be false"); + ok(!paymentOptions.requestShipping, "requestShipping option should be false"); + is( + paymentOptions.shippingType, + "shipping", + "shippingType option should be 'shipping'" + ); +} + +function checkDupShippingOptionsPayment(aPayment) { + // checking the passed PaymentMethods parameter + is(aPayment.paymentMethods.length, 1, "paymentMethods' length should be 1."); + + const methodData = aPayment.paymentMethods.queryElementAt( + 0, + Ci.nsIPaymentMethodData + ); + ok(methodData, "Fail to get payment methodData."); + is( + methodData.supportedMethods, + "basic-card", + "methodData.supportedMethod name should be 'basic-card'." + ); + ok(!methodData.data, "methodData.data should not exist."); + + // checking the passed PaymentDetails parameter + const details = aPayment.paymentDetails; + is( + details.id, + "duplicate shipping options details", + "details.id should be 'duplicate shipping options details'." + ); + is( + details.totalItem.label, + "Donation", + "total item's label should be 'Donation'." + ); + is( + details.totalItem.amount.currency, + "USD", + "total item's currency should be 'USD'." + ); + is( + details.totalItem.amount.value, + "55.00", + "total item's value should be '55.00'." + ); + + const shippingOptions = details.shippingOptions; + is(shippingOptions.length, 0, "shippingOptions' length should be 0."); + + // checking the passed PaymentOptions parameter + const paymentOptions = aPayment.paymentOptions; + ok(paymentOptions.requestPayerName, "payerName option should be true"); + ok(paymentOptions.requestPayerEmail, "payerEmail option should be true"); + ok(paymentOptions.requestPayerPhone, "payerPhone option should be true"); + ok(paymentOptions.requestShipping, "requestShipping option should be true"); + is( + paymentOptions.shippingType, + "shipping", + "shippingType option should be 'shipping'" + ); +} diff --git a/dom/payments/test/mochitest.ini b/dom/payments/test/mochitest.ini new file mode 100644 index 0000000000..2b302feed6 --- /dev/null +++ b/dom/payments/test/mochitest.ini @@ -0,0 +1,53 @@ +[DEFAULT] +prefs = + dom.payments.request.enabled=true +# Android crashes on nearly all tests, bug 1525959 +skip-if = + true # we don't ship webpayments right now bug 1514425 +scheme = https +support-files = + blank_page.html + bug1478740.html + simple_payment_request.html + echo_payment_request.html + BasiccardChromeScript.js + Bug1478740ChromeScript.js + BasicCardErrorsChromeScript.js + Bug1490698ChromeScript.js + ClosePaymentChromeScript.js + ConstructorChromeScript.js + CurrencyAmountValidationChromeScript.js + DefaultData.js + GeneralChromeScript.js + PayerDetailsChromeScript.js + PMIValidationChromeScript.js + RequestShippingChromeScript.js + RetryPaymentChromeScript.js + ShippingOptionsChromeScript.js + ShowPaymentChromeScript.js + UpdateErrorsChromeScript.js + +[test_abortPayment.html] +run-if = nightly_build # Bug 1390018: Depends on the Nightly-only UI service +skip-if = debug # Bug 1507251 - Leak +[test_basiccard.html] +[test_basiccarderrors.html] +[test_block_none10s.html] +skip-if = true # Bug 1408250: Don't expose PaymentRequest Constructor in non-e10s +[test_bug1478740.html] +[test_bug1490698.html] +[test_canMakePayment.html] +run-if = nightly_build # Bug 1390737: Depends on the Nightly-only UI service +skip-if = debug # Bug 1507251 - Leak +[test_closePayment.html] +[test_constructor.html] +skip-if = (os == "linux") || (os == "mac") || (os == "win" && os_version == "10.0") # Bug 1514425 +[test_currency_amount_validation.html] +[test_payerDetails.html] +[test_payment-request-in-iframe.html] +[test_pmi_validation.html] +[test_requestShipping.html] +[test_retryPayment.html] +[test_shippingOptions.html] +[test_showPayment.html] +[test_update_errors.html] diff --git a/dom/payments/test/simple_payment_request.html b/dom/payments/test/simple_payment_request.html new file mode 100644 index 0000000000..b532ba6101 --- /dev/null +++ b/dom/payments/test/simple_payment_request.html @@ -0,0 +1,81 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Payment Request Testing</title> +<script> +const methods = [ + { + supportedMethods: "basic-card", + }, +]; +const details = { + id: "simple details", + total: { + label: "Donation", + amount: { currency: "USD", value: "55.00" }, + }, +}; +const updatedDetails = { + id: "simple details", + total: { + label: "Donation", + amount: { currency: "USD", value: "55.00" }, + }, + error: "", +}; + +let request; +let shippingChangedEvent; + +let msg = "successful"; +try { + request = new PaymentRequest(methods, details); + request.onshippingoptionchange = (event) => { + shippingChangedEvent = event; + window.parent.postMessage("successful", "*"); + }; + request.onshippingaddresschange = (event) => { + shippingChangedEvent = event; + window.parent.postMessage("successful", "*"); + }; + +} catch (err) { + msg = err.name; +} +window.parent.postMessage(msg, "*"); + + +if (request) { + window.onmessage = async ({ data: action }) => { + msg = "successful"; + switch (action) { + case "show PaymentRequest": + const responsePromise = request.show(); + window.parent.postMessage(msg, "*"); + try { + await responsePromise; + } catch (err) { + if (err.name !== "AbortError") { + msg = err.name; + } + } + window.parent.postMessage(msg, "*") + break; + case "updateWith PaymentRequest": + if (shippingChangedEvent) { + try { + shippingChangedEvent.updateWith(updatedDetails); + } catch(err) { + if (err.name !== "InvalidStateError") { + msg = err.name; + } + } + window.parent.postMessage(msg, "*"); + shippingChangedEvent = undefined; + } + break; + default: + window.parent.postMessage(`fail - unknown postmessage action: ${action}`, "*"); + } + }; +} +</script> diff --git a/dom/payments/test/test_abortPayment.html b/dom/payments/test/test_abortPayment.html new file mode 100644 index 0000000000..64285914aa --- /dev/null +++ b/dom/payments/test/test_abortPayment.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1345367 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1345367</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('GeneralChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + const defaultMethods = [{ + supportedMethods: "basic-card", + }]; + const defaultDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + }; + + function testBeforeShow() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.abort().then((result) => { + ok(false, "Should throw 'InvalidStateError', but got resolved."); + resolve(); + }).catch((err) => { + is(err.name, "InvalidStateError", + "Expected 'InvalidStateError', but got '" + err.name + "'"); + resolve(); + }); + }); + } + + function testAfterShow() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + const acceptPromise = payRequest.show(); + payRequest.abort().then((abortResult) => { + is(abortResult, undefined, "Should be resolved with undefined."); + resolve(); + }).catch( (err) => { + ok(false, "Expected no error, but got '" + err.name + "'."); + resolve(); + }).finally(handler.destruct); + }); + } + + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testBeforeShow() + .then(testAfterShow) + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345367">Mozilla Bug 1345367</a> +</pre> +</body> +</html> diff --git a/dom/payments/test/test_basiccard.html b/dom/payments/test/test_basiccard.html new file mode 100644 index 0000000000..e8d30fbd06 --- /dev/null +++ b/dom/payments/test/test_basiccard.html @@ -0,0 +1,371 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1375345 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1375345</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('BasiccardChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + gScript.sendAsyncMessage(action, params); + }); + } + + const errorNetworksMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ["myNetwork"], + }, + }]; + + const nullDataMethods = [{ + supportedMethods: "basic-card", + }]; + + const emptyDataMethods = [{ + supportedMethods: "basic-card", + data: {}, + }]; + + const unconvertableDataMethods = [{ + supportedMethods: "basic-card", + data: "unconvertable data", + }]; + + const defaultMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ["unionpay", "visa", "mastercard", "amex", "discover", + "diners", "jcb", "mir", + ], + }, + }]; + const defaultDetails = { + id: "test payment", + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: true, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + }; + + const updateDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: true, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + error: "", + }; + + const defaultOptions = { + requestPayerName: true, + requestPayerEmail: false, + requestPayerPhone: false, + requestShipping: true, + shippingType: "shipping" + }; + + async function testBasicCardRequestWithErrorNetworks() { + const testName = "testBasicCardRequestWithErrorNetworks"; + try { + const request = new PaymentRequest(errorNetworksMethods, defaultDetails, defaultOptions); + ok(false, `${testName}: Expected 'TypeError', but got success construction.`); + } catch (e) { + is(e.name, "TypeError", `${testName}: Expected TypeError, but got ${e.name}`); + } + } + + async function testBasicCardRequestWithUnconvertableData() { + const testName = "testBasicCardRequestWithUnconvertableData"; + try { + const request = new PaymentRequest(unconvertableDataMethods, defaultDetails, defaultOptions); + ok(false, `${testName}: Expected 'TypeError', but got success construction.`); + } catch (e) { + is(e.name, "TypeError", `${testName}: Expected TypeError, but got ${e.name}`); + } + } + + async function testBasicCardRequestWithNullData() { + const testName = "testBasicCardRequestWithNullData"; + try { + const request = new PaymentRequest(nullDataMethods, defaultDetails, defaultOptions); + ok(request, `${testName}: PaymentRequest should be constructed with null data BasicCardRequest.`); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + } + + async function testBasicCardRequestWithEmptyData() { + const testName = "testBasicCardRequestWithEmptyData"; + try { + const request = new PaymentRequest(emptyDataMethods, defaultDetails, defaultOptions); + ok(request, `${testName}: PaymentRequest should be constructed with empty data BasicCardRequest.`); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + } + + async function testCanMakePaymentWithBasicCardRequest() { + const testName = "testCanMakePaymentWithBasicCardRequest"; + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + try { + const result = await request.canMakePayment(); + ok(result, `${testName}: canMakePayment() should be resolved with true.`); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + } + + async function testBasicCardSimpleResponse() { + const testName = "testBasicCardSimpleResponse"; + await requestChromeAction("set-simple-ui-service", testName); + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + ok(response.details, `${testName}: basiccard response should exists.`); + ok(!response.details.cardholderName, `${testName}: response.details.cardholderName should not exist.`); + is(response.details.cardNumber, "4916855166538720", + `${testName}: response.details.cardNumber should be '4916855166538720'.`); + ok(!response.details.expiryMonth, `${testName}: response.details.expiryMonth should not exist.`); + ok(!response.details.expiryYear, `${testName}: response.details.expiryYear should be '2024'.`); + ok(!response.details.cardSecurityCode, `${testName}: response.details.cardSecurityCode should not exist.`); + ok(!response.details.billingAddress, `${testName}: response.details.billingAddress should not exist.`); + await response.complete("success"); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + await handler.destruct(); + } + + async function testBasicCardDetailedResponse() { + const testName = "testBasicCardDetailedResponse"; + await requestChromeAction("set-detailed-ui-service", testName); + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + ok(response.details, `${testName}: basiccard response should exists.`); + ok(response.details.cardholderName, `${testName}: response.details.cardholderName should not exist.`); + is(response.details.cardNumber, "4916855166538720", + `${testName}: response.details.cardNumber should be '4916855166538720'.`); + ok(response.details.expiryMonth, `${testName}: response.details.expiryMonth should not exist.`); + ok(response.details.expiryYear, `${testName}: response.details.expiryYear should be '2024'.`); + ok(response.details.cardSecurityCode, `${testName}: response.details.cardSecurityCode should not exist.`); + ok(response.details.billingAddress, `${testName}: response.details.billingAddress should not exist.`); + const billingAddress = response.details.billingAddress; + is(billingAddress.country, "USA", `${testName}: country should be 'USA'.`); + is(billingAddress.addressLine.length, 1, `${testName}: addressLine.length should be 1.`); + is(billingAddress.addressLine[0], "Easton Ave", `${testName}: addressLine[0] should be 'Easton Ave'.`); + is(billingAddress.region, "CA", `${testName}: region should be 'CA'.`); + is(billingAddress.regionCode, "CA", `${testName}: regionCode should be 'CA'.`); + is(billingAddress.city, "San Bruno", `${testName}: city should be 'San Bruno'.`); + is(billingAddress.dependentLocality, "", `${testName}: dependentLocality should be empty.`); + is(billingAddress.postalCode, "94066", `${testName}: postalCode should be '94066'.`); + is(billingAddress.sortingCode, "123456", `${testName}: sortingCode should be '123456'.`); + is(billingAddress.organization, "", `${testName}: organization should be empty.`); + is(billingAddress.recipient, "Bill A. Pacheco", `${testName}: recipient should be 'Bill A. Pacheco'.`); + is(billingAddress.phone, "+14344413879", `${testName}: phone should be '+14344413879'.`); + await response.complete("success"); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + await handler.destruct(); + } + + async function testSpecialAddressResponse() { + const testName = "testSpecialAddressResponse"; + await requestChromeAction("set-special-address-ui-service", testName); + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + ok(response.details, `${testName}: BasiccardResponse should exist.`); + ok(response.details.billingAddress, + `${testName}: BasiccardResponse.billingAddress should exist.`); + is(response.details.billingAddress.addressLine[0], ":$%@&*", + `${testName}: AddressLine should be ':$%@&*'`); + await response.complete("success"); + } catch (e) { + ok(false, `${testName}: Unexpected error: ${e.name}`); + } + await handler.destruct(); + } + + async function testMethodChangeWithoutRequestBillingAddress() { + const testName = "testMethodChangeWithoutRequestBillingAddress"; + await requestChromeAction("method-change-to-basic-card", testName); + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + request.addEventListener("paymentmethodchange", async (event) => { + is(event.methodName, "basic-card", `${testName}: PaymentMethodChangeEvent.methodName should be 'basic-card'.`) + ok(event.methodDetails, `PaymentMethodChangeEvent.methodDetails should exist.`); + ok(!event.methodDetails.billingAddress, `PaymentMethodChangeEvent.methodDetails.billingAddres should not exist.`); + event.updateWith(updateDetails); + }); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + await response.complete("success"); + } catch (error) { + ok(false, `${testName}: Unexpected error: ${error.name}`); + } + await handler.destruct(); + } + + async function testMethodChangeWithRequestBillingAddress() { + const testName = "testMethodChangeWithRequestBillingAddress"; + await requestChromeAction("method-change-to-basic-card", testName); + const options = { + requestPayerName: true, + requestBillingAddress: true, + requestShipping: true, + shippingType: "shipping", + }; + const request = new PaymentRequest(defaultMethods, defaultDetails, options); + request.addEventListener("paymentmethodchange", async (event) => { + is(event.methodName, "basic-card", `${testName}: PaymentMethodChangeEvent.methodName should be 'basic-card'.`) + ok(event.methodDetails, `PaymentMethodChangeEvent.methodDetails should exist.`); + const billingAddress = event.methodDetails.billingAddress; + is(billingAddress.country, "USA", `${testName}: country should be 'USA'.`); + is(billingAddress.addressLine.length, 1, `${testName}: addressLine.length should be 1.`); + is(billingAddress.addressLine[0], "Easton Ave", `${testName}: addressLine[0] should be 'Easton Ave'.`); + is(billingAddress.region, "CA", `${testName}: region should be 'CA'.`); + is(billingAddress.regionCode, "CA", `${testName}: regionCode should be 'CA'.`); + is(billingAddress.city, "San Bruno", `${testName}: city should be 'San Bruno'.`); + is(billingAddress.dependentLocality, "", `${testName}: dependentLocality should be empty.`); + is(billingAddress.postalCode, "94066", `${testName}: postalCode should be '94066'.`); + is(billingAddress.sortingCode, "123456", `${testName}: sortingCode should be '123456'.`); + is(billingAddress.organization, "", `${testName}: organization should be empty.`); + is(billingAddress.recipient, "Bill A. Pacheco", `${testName}: recipient should be 'Bill A. Pacheco'.`); + is(billingAddress.phone, "+14344413879", `${testName}: phone should be '+14344413879'.`); + event.updateWith(updateDetails); + }); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + await response.complete("success"); + } catch (error) { + ok(false, `${testName}: Unexpected error: ${error.name}`); + } + await handler.destruct(); + } + + + async function testBasicCardErrorResponse() { + const testName = "testBasicCardErrorResponse"; + return requestChromeAction("error-response-test", testName); + } + + async function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler) + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + async function runTests() { + try { + await testBasicCardRequestWithErrorNetworks(); + await testBasicCardRequestWithUnconvertableData(); + await testBasicCardRequestWithNullData(); + await testBasicCardRequestWithEmptyData(); + await testCanMakePaymentWithBasicCardRequest(); + await testBasicCardSimpleResponse(); + await testBasicCardDetailedResponse(); + await testSpecialAddressResponse(); + await testBasicCardErrorResponse(); + await testMethodChangeWithoutRequestBillingAddress(); + await testMethodChangeWithRequestBillingAddress() + await teardown(); + } catch (e) { + ok(false, `test_basiccard.html: Unexpected error: ${e.name}`); + SimpleTest.finish(); + }; + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1375345">Mozilla Bug 1375345</a> +</body> +</html> diff --git a/dom/payments/test/test_basiccarderrors.html b/dom/payments/test/test_basiccarderrors.html new file mode 100644 index 0000000000..f9ac76ae75 --- /dev/null +++ b/dom/payments/test/test_basiccarderrors.html @@ -0,0 +1,85 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1489968 +--> +<meta charset="utf-8"> +<title>Test for Bug 1489968</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="./DefaultData.js"></script> +<script> +SimpleTest.waitForExplicitFinish(); + +const gUrl = SimpleTest.getTestFileURL("BasicCardErrorsChromeScript.js"); +const gScript = SpecialPowers.loadChromeScript(gUrl); + +function sendOnce(message) { + return data => { + return new Promise(resolve => { + const doneMsg = `${message}-complete`; + gScript.addMessageListener(doneMsg, function listener() { + gScript.removeMessageListener(doneMsg, listener); + resolve(); + }); + gScript.sendAsyncMessage(message, data); + }); + }; +} +const sendTearDown = sendOnce("teardown"); + +async function teardown() { + await sendTearDown(); + gScript.destroy(); + SimpleTest.finish(); +} + +async function testBasicCardErrors() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput( + true + ); + const request = new PaymentRequest( + [{ supportedMethods: "basic-card" }], + defaultDetails + ); + const response = await request.show(); + // Smoke test the initial state + is(response.details.cardNumber, "4111111111111111", "Expected cardNumber to initially be 4111111111111111"); + // We send these up and have the chrome script echo them back to us. + const expected = { + cardholderName: "PASS", + cardNumber: "3566002020360505", + cardSecurityCode: "666", + expiryMonth: "02", + expiryYear: "2020", + }; + await response.retry({ paymentMethod: expected }); + // the values of the response would have been updated with the expected + for (const [member, expectedValue] of Object.entries(expected)) { + const actual = response.details[member]; + is( + actual, + expectedValue, + `Expected member ${member} to be "${expectedValue}, but got "${actual}"` + ); + } + await response.complete("success"); + handler.destruct(); +} + +async function runTests() { + try { + await testBasicCardErrors(); + } catch (err) { + ok(false, `Unexpected error: ${err} ${err.stack}.`); + } finally { + await teardown(); + } +} + +window.addEventListener("load", () => { + const prefs = [["dom.payments.request.enabled", true]]; + SpecialPowers.pushPrefEnv({ set: prefs }, runTests); +}); +</script> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1489968">Mozilla Bug 1489968</a> diff --git a/dom/payments/test/test_block_none10s.html b/dom/payments/test/test_block_none10s.html new file mode 100644 index 0000000000..b1d654f38c --- /dev/null +++ b/dom/payments/test/test_block_none10s.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for Bug 1408250</title> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <script type="text/javascript"> + "use strict"; + SimpleTest.waitForExplicitFinish(); + + function testInNone10s() { + return new Promise((resolve,reject) => { + const supportedInstruments = [{ + supportedMethods: "basic-card", + }]; + const details = { + id: "simple details", + total: { + label: "Donation", + amount: { currency: "USD", value: "55.00" } + }, + }; + try { + const payRequest = new PaymentRequest(supportedInstruments, details); + ok(false, "Unexpected, new PaymentRequest() can not be used in non-e10s."); + } catch (err) { + ok(err.name, "ReferenceError", + "Expected ReferenceError when calling new PaymentRequest()"); + } + resolve(); + + }); + } + + function runTests() { + testInNone10s() + .then(SimpleTest.finish) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + </script> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1408250">Mozilla Bug 1408250</a> + </body> +</html> diff --git a/dom/payments/test/test_bug1478740.html b/dom/payments/test/test_bug1478740.html new file mode 100644 index 0000000000..e877face76 --- /dev/null +++ b/dom/payments/test/test_bug1478740.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1478740 +--> +<head> + <meta charset="utf-8"> + <title>Test for retry PaymentRequest</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + const gUrl = SimpleTest.getTestFileURL('Bug1478740ChromeScript.js'); + const gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + gScript.sendAsyncMessage(action, params); + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + }); + } + function unexpectedErrMsg(testName, errName, timing) { + return `${testName}: Unexpected error(${errName}) when ${timing} the PaymentRequest.`; + } + + async function testMultipleShows() { + const testName = "testMultipleShows"; + await requestChromeAction("start-test", testName); + let expectedResults = ["successful", + "successful", + "successful", + "AbortError", + "AbortError", + "AbortError"]; + let nextStatus = ["creating first page", + "creating second page", + "showing first payment", + "showing second payment", + "showing third payment", + "aborting first payment"]; + let currStatus = nextStatus.shift(); + let ifr1 = document.createElement('iframe'); + let ifr2 = document.createElement('iframe'); + + await new Promise(resolve => { + let listener = async function(event) { + let expected = expectedResults.shift(); + is(event.data, expected, + `${testName}: Expected '${expected}' when ${currStatus}, but got '${event.data}'`); + switch (currStatus) { + case "creating first page": + ifr2.src = "bug1478740.html"; + document.body.appendChild(ifr2); + break; + case "creating second page": + ifr1.contentWindow.postMessage("Show Payment", "*"); + break; + case "showing first payment": + ifr2.contentWindow.postMessage("Show Payment", "*"); + break; + case "showing second payment": + ifr2.contentWindow.postMessage("Show Payment", "*"); + break; + case "showing third payment": + await requestChromeAction("reject-payment"); + break; + case "aborting first payment": + window.removeEventListener("message", listener); + gScript.removeMessageListener("showing-payment", listener); + document.body.removeChild(ifr1); + document.body.removeChild(ifr2); + resolve(); + break; + default: + ok(false, `unknown status ${currStatus}`); + } + currStatus = nextStatus.shift(); + } + window.addEventListener("message", listener); + gScript.addMessageListener("showing-payment", listener); + ifr1.src = "bug1478740.html"; + document.body.appendChild(ifr1); + }); + await requestChromeAction("finish-test"); + } + + function teardown() { + return new Promise((resolve, reject) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function runTests() { + try { + await testMultipleShows(); + await teardown(); + } catch(e) { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + } + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ['dom.payments.request.user_interaction_required', false], + ] + }, runTests); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1478740">Mozilla Bug 1478740</a> +</body> +</html> diff --git a/dom/payments/test/test_bug1490698.html b/dom/payments/test/test_bug1490698.html new file mode 100644 index 0000000000..e1126af770 --- /dev/null +++ b/dom/payments/test/test_bug1490698.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1490698 +--> +<head> + <meta charset="utf-8"> + <title>Test for retry PaymentRequest</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + const gUrl = SimpleTest.getTestFileURL('Bug1490698ChromeScript.js'); + const gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + gScript.sendAsyncMessage(action, params); + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + }); + } + function unexpectedErrMsg(testName, errName, timing) { + return `${testName}: Unexpected error(${errName}) when ${timing} the PaymentRequest.`; + } + + async function testInteractWithPaymentUnderWrongState() { + const testName = "testInteractWithPaymentUnderWrongState"; + await requestChromeAction("start-test", testName); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + await requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + info(`${testName}: Interact with payment when PaymentRequest is eClosed`); + await requestChromeAction("interact-with-payment"); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "showing")); + handler.destruct(); + await requestChromeAction("finish-test"); + return; + } + try { + await payResponse.complete("success"); + ok(true, `${testName}: complete() is successful after PaymentRequest's state is eClosed.`); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "completing")); + await requestChromeAction("finish-test"); + return; + } + try { + info(`${testName}: Interact with payment when PaymentRequest is completed`); + await requestChromeAction("interact-with-payment"); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "completing")); + await requestChromeAction("finish-test"); + return; + } + await requestChromeAction("finish-test"); + } + + function teardown() { + return new Promise((resolve, reject) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function runTests() { + try { + await testInteractWithPaymentUnderWrongState(); + await teardown(); + } catch(e) { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + } + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1490698">Mozilla Bug 1490698</a> +</body> +</html> diff --git a/dom/payments/test/test_canMakePayment.html b/dom/payments/test/test_canMakePayment.html new file mode 100644 index 0000000000..112fb8ce72 --- /dev/null +++ b/dom/payments/test/test_canMakePayment.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1345365 +--> +<head> + <meta charset="utf-8"> + <title>Test for PaymentRequest API canMakePayment()</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('GeneralChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + const defaultMethods = [{ + supportedMethods: "basic-card", + }]; + const defaultDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + }; + + const nonsupportedMethods = [{ + supportedMethods: "testing-payment-method", + }]; + + function testDefaultAction() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(nonsupportedMethods, defaultDetails); + payRequest.canMakePayment().then((result) => { + ok(!result, "Should be resolved with false, but got " + result + "."); + resolve(); + }).catch((err) => { + ok(false, "Expected no error, but got '" + err.name +"'."); + resolve(); + }); + }); + } + + function testSimple() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.canMakePayment().then((result) => { + ok(result, "Should be resolved with true, but got " + result + "."); + resolve(); + }).catch((err) => { + ok(false, "Expected no error, but got '" + err.name +"'."); + resolve(); + }); + }); + } + + function testAfterShow() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + const acceptPromise = payRequest.show(); + payRequest.canMakePayment().then((result) => { + ok(false, "Should throw 'InvalidStateError', but got resolved."); + resolve(); + }).catch( (err) => { + is(err.name, "InvalidStateError", + "Expected 'InvalidStateError', but got '" + err.name + "'."); + payRequest.abort().then((abortResult) => { + is(abortResult, undefined, "abort() should be resolved with undefined."); + resolve(); + }).catch( (error) => { + ok(false, "Expected no error, but got '" + error.name + "'."); + resolve(); + }).finally(handler.destruct); + }); + }); + } + + function testAfterAbort() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + const acceptPromise = payRequest.show(); + payRequest.abort().then((abortResult) => { + payRequest.canMakePayment().then((result) => { + ok(false, "Should throw 'InvalidStateError', but got resolved."); + resolve(); + }).catch( (err) => { + is(err.name, "InvalidStateError", + "Expected 'InvalidStateError', but got '" + err.name + "'."); + resolve(); + }); + }).catch( (err) => { + ok(false, "Expected no error, but got '" + err.name +"'."); + resolve(); + }).finally(handler.destruct); + }); + } + + async function testNotAllowed() { + let payRequest = new PaymentRequest(defaultMethods, defaultDetails); + for (let i = 0; i < 1000; i++) { + try { + await payRequest.canMakePayment(); + } catch(err) { + is(err.name, "NotAllowedError", + "Expected 'NotAllowError', but got '" + err.name + "'"); + break; + } + } + for (let i = 0; i < 1000; i++) { + try { + await new PaymentRequest(defaultMethods, defaultDetails).canMakePayment(); + } catch(err) { + is(err.name, "NotAllowedError", + "Expected 'NotAllowError', but got '" + err.name + "'"); + break; + } + } + } + + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testDefaultAction() + .then(testSimple) + .then(testAfterShow) + .then(testAfterAbort) + .then(testNotAllowed) + .then(teardown) + .catch(e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345365">Mozilla Bug 1345365</a> +</body> +</html> diff --git a/dom/payments/test/test_closePayment.html b/dom/payments/test/test_closePayment.html new file mode 100644 index 0000000000..8f2ad7cd00 --- /dev/null +++ b/dom/payments/test/test_closePayment.html @@ -0,0 +1,284 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1408234 +--> +<head> + <meta charset="utf-8"> + <title>Test for closing PaymentRequest</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('ClosePaymentChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + gScript.sendAsyncMessage(action, params); + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + }); + } + + async function testCloseByReloading() { + const testName = "testCloseByReloading"; + await requestChromeAction("test-setup", testName); + info(testName); + let nextStatus = ["creating", "reloading"]; + let currStatus = nextStatus.shift(); + let ifr = document.createElement('iframe'); + await requestChromeAction("payment-num-set"); + + await new Promise((resolve) => { + let listener = function(event) { + is(event.data, "successful", + `${testName}: Expected 'successful' when ${currStatus}, but got '${event.data}'.`); + if (currStatus === "creating") { + ifr.contentWindow.location.reload(); + } else if (currStatus === "reloading") { + window.removeEventListener("message", listener); + resolve(); + } + currStatus = nextStatus.shift(); + } + window.addEventListener("message", listener); + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + }); + + await requestChromeAction("payment-num-check", 1); + document.body.removeChild(ifr); + + } + + async function testCloseByRedirecting() { + const testName = "testCloseByRedirecting"; + await requestChromeAction("test-setup", testName); + return new Promise((resolve) => { + let nextStatus = ["creating", "redirecting"]; + let currStatus = nextStatus.shift(); + let ifr = document.createElement('iframe'); + let listener = async function(event) { + is(event.data, "successful", + `${testName}: Expected 'successful' when ${currStatus}, but got '${event.data}'.`); + if (currStatus === "creating") { + ifr.src = "blank_page.html"; + } else if (currStatus === "redirecting"){ + window.removeEventListener("message", listener); + await requestChromeAction("close-check"); + document.body.removeChild(ifr); + resolve(); + } + currStatus = nextStatus.shift(); + }; + window.addEventListener("message", listener); + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + }); + } + + async function testCloseByRedirectingAfterShow() { + const testName = "testCloseByRedirectingAfterShow"; + await requestChromeAction("test-setup", testName); + return new Promise((resolve) => { + let nextStatus = ["creating", "showing", "redirecting"]; + let currStatus = nextStatus.shift(); + let ifr = document.createElement('iframe'); + let handler = undefined; + let listener = async (event) => { + is(event.data, "successful", + `${testName}: Expected 'successful' when ${currStatus}, but got '${event.data}'.`); + if (currStatus === "creating") { + handler = SpecialPowers.getDOMWindowUtils(ifr.contentWindow).setHandlingUserInput(true); + ifr.contentWindow.postMessage("show PaymentRequest", "*"); + } else if (currStatus === "showing") { + handler.destruct(); + ifr.src = "blank_page.html"; + } else if (currStatus === "redirecting") { + window.removeEventListener("message", listener); + await requestChromeAction("close-check"); + await requestChromeAction("reject-payment", true); + document.body.removeChild(ifr); + resolve(); + } + currStatus = nextStatus.shift(); + } + window.addEventListener("message", listener); + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + }); + } + + async function testCloseByRemovingIframe() { + const testName = "testCloseByRemovingIframe"; + await requestChromeAction("test-setup", testName); + return new Promise((resolve) => { + let nextStatus = ["creating"]; + let currStatus = nextStatus.shift(); + let ifr = document.createElement('iframe'); + let listener = async function(event) { + is(event.data, "successful", + `${testName}: Expected 'successful' when ${currStatus}, but got '${event.data}'.`); + document.body.removeChild(ifr); + window.removeEventListener("message", listener); + await requestChromeAction("close-check"); + resolve(); + }; + window.addEventListener("message", listener); + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + }); + } + + async function testUpdateWithRespondedPayment() { + const testName = "testUpdateWithRespondedPayment"; + await requestChromeAction("test-setup", testName); + return new Promise(resolve => { + let nextStatus = ["creating", "showing", "closing", "updating", "finishing"]; + let currStatus = nextStatus.shift(); + let ifr = document.createElement('iframe'); + let handler = undefined; + let listener = async function(event) { + is(event.data, "successful", + `${testName}: Expected 'successful' when ${currStatus}, but got '${event.data}'.`); + switch (currStatus) { + case "creating": + handler = SpecialPowers.getDOMWindowUtils(ifr.contentWindow).setHandlingUserInput(true); + ifr.contentWindow.postMessage("show PaymentRequest", "*"); + break; + case "showing": + await requestChromeAction("update-payment"); + break; + case "closing": + await requestChromeAction("reject-payment", false); + break; + case "updating": + await requestChromeAction("close-check"); + ifr.contentWindow.postMessage("updateWith PaymentRequest", "*"); + break; + case "finishing": + handler.destruct(); + document.body.removeChild(ifr); + window.removeEventListener("message", listener); + resolve(); + break; + default: + ok(false, testName + ": Unknown status()" + currStatus); + break; + } + currStatus = nextStatus.shift(); + } + window.addEventListener("message", listener); + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + }); + } + + function getLoadedPaymentRequest(iframe, url) { + return new Promise(resolve => { + iframe.addEventListener( + "load", + () => { + const { PaymentRequest } = iframe.contentWindow; + const request = new PaymentRequest(defaultMethods, defaultDetails); + resolve(request); + }, + { once: true } + ); + iframe.src = url; + }); + } + + async function testNonfullyActivePayment() { + const testName = "testNonfullyActivePayment"; + await requestChromeAction("test-setup", testName); + + const outer = document.createElement("iframe"); + outer.allow = "payment"; + document.body.appendChild(outer); + await getLoadedPaymentRequest(outer,"blank_page.html"); + + const inner = outer.contentDocument.createElement("iframe"); + inner.allow = "payment"; + outer.contentDocument.body.appendChild(inner); + + const request = await getLoadedPaymentRequest(inner,"blank_page.html"); + ok(request, `${testName}: PaymentRequest in inner iframe should exist.`); + + await new Promise(res => { + outer.addEventListener("load", res); + outer.src = "simple_payment_request.html"; + }); + + let handler = SpecialPowers.getDOMWindowUtils(inner.contentWindow).setHandlingUserInput(true); + try { + const showPromise = await request.show(); + ok(false, `${testName}: expected 'AbortError', but got resolved.`); + } catch (error) { + is(error.name, "AbortError", + `${testName}: expected 'AbortError'.`); + } + await handler.destruct(); + inner.remove(); + outer.remove(); + } + + async function teardown() { + return new Promise((resolve) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function runTests() { + try { + await testCloseByReloading(); + await testCloseByRedirecting(); + await testCloseByRedirectingAfterShow(); + await testCloseByRemovingIframe(); + await testUpdateWithRespondedPayment(); + await testNonfullyActivePayment(); + await teardown(); + } catch(e) { + ok(false, "test_closePayment.html: Unexpected error: " + e.name); + SimpleTest.finish(); + } + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1408234">Mozilla Bug 1408234</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1483470">Mozilla Bug 1483470</a> +</body> +</html> diff --git a/dom/payments/test/test_constructor.html b/dom/payments/test/test_constructor.html new file mode 100644 index 0000000000..4517a37028 --- /dev/null +++ b/dom/payments/test/test_constructor.html @@ -0,0 +1,351 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1345361 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1345361</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('ConstructorChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + + const simplestMethods = [{ + supportedMethods: "basic-card", + }]; + const simplestDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + } + }; + + const complexMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ['unionpay', 'visa', 'mastercard', 'amex', 'discover', + 'diners', 'jcb', 'mir', + ], + }, + }]; + + const nonBasicCardMethods = [{ + supportedMethods: "testing-payment-method", + data: { + paymentId: "P3892940", + paymentType: "prepaid", + }, + }]; + + const complexDetails = { + id: "payment details", + total: { + label: "Total", + amount: { + currency: "USD", + value: "100.00" + } + }, + displayItems: [ + { + label: "First item", + amount: { + currency: "USD", + value: "60.00" + } + }, + { + label: "Second item", + amount: { + currency: "USD", + value: "40.00" + } + } + ], + modifiers: [ + { + supportedMethods: "basic-card", + total: { + label: "Discounted Total", + amount: { + currency: "USD", + value: "90.00" + } + }, + additionalDisplayItems: [ + { + label: "basic-card discount", + amount: { + currency: "USD", + value: "-10.00" + } + } + ], + data: { discountProgramParticipantId: "86328764873265", } + }, + ], + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: true, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + }; + + const complexOptions = { + requestPayerName: true, + requestPayerEmail: true, + requestPayerPhone: true, + requestShipping: true, + shippingType: "shipping" + }; + + const duplicateShippingOptionsDetails = { + id: "duplicate shipping options details", + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + shippingOptions: [ + { + id: "dupShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: true, + }, + { + id: "dupShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + }; + + + function testWithSimplestParameters() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(simplestMethods, simplestDetails); + ok(payRequest, "PaymentRequest should be created"); + gScript.addMessageListener("check-complete", function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + }); + gScript.sendAsyncMessage("check-simplest-request"); + }); + } + + function testWithComplexParameters() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(complexMethods, complexDetails, complexOptions); + ok(payRequest, "PaymentRequest should be created"); + gScript.addMessageListener("check-complete", function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + }); + gScript.sendAsyncMessage("check-complex-request"); + }); + } + + function testWithNonBasicCardMethods() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(nonBasicCardMethods, simplestDetails); + ok(payRequest, "PaymentRequest should be created"); + gScript.addMessageListener("check-complete", function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + }); + gScript.sendAsyncMessage("check-nonbasiccard-request"); + }); + } + + function testWithDuplicateShippingOptionsParameters() { + return new Promise((resolve, reject) => { + try { + const payRequest = new PaymentRequest(simplestMethods, + duplicateShippingOptionsDetails, + {requestShipping: true}); + ok(false, "Construction should fail with duplicate shippingOption Ids."); + resolve(); + } catch (e) { + is(e.name, "TypeError", "Expected 'TypeError' with duplicate shippingOption Ids."); + resolve(); + } + }); + } + + function testShippingOptionAttribute() { + return new Promise((resolve, reject) => { + const details = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + shippingOptions: [ + { + id: "option1", + label: "option1", + amount: { + currency: "USD", + value: "1.00", + }, + selected: false, + }, + { + id: "option2", + label: "option2", + amount: { + currency: "USD", + value: "1.00", + }, + selected: false, + }, + ], + }; + const payRequest1 = new PaymentRequest(simplestMethods, + details, + {requestShipping: false}); + ok(payRequest1, "PaymentRequest should be created"); + is(payRequest1.shippingOption, null, + "request.shippingOption should be null in default, when options.requestShipping is false"); + details.shippingOptions[0].selected = true; + const payRequest2 = new PaymentRequest(simplestMethods, + details, + {requestShipping: false}); + ok(payRequest2, "PaymentRequest should be created"); + is(payRequest2.shippingOption, null, + "request.shippingOption should be null in default, when options.requestShipping is false"); + const payRequest3 = new PaymentRequest(simplestMethods, + details, + {requestShipping: true}); + ok(payRequest3, "PaymentRequest should be created"); + ok(payRequest3.shippingOption, + "request.shippingOption should not be null when both shoppingOtpion.selected and options.requestOptions are true"); + is(payRequest3.shippingOption, "option1", + "request.shippingOption should be 'option1'"); + details.shippingOptions[1].selected = true; + const payRequest4 = new PaymentRequest(simplestMethods, + details, + {requestShipping: true}); + ok(payRequest4, "PaymentRequest should be created"); + ok(payRequest4.shippingOption, + "request.shippingOption should not be null when both shoppingOtpion.selected and options.requestOptions are true"); + is(payRequest4.shippingOption, "option2", + "request.shippingOption should be 'option2' which is the last one selected."); + resolve(); + }); + } + + function testMultipleRequests() { + return new Promise((resolve, reject) => { + const payRequest1 = new PaymentRequest(complexMethods, complexDetails, complexOptions); + const payRequest2 = new PaymentRequest(simplestMethods, simplestDetails); + ok(payRequest1, "PaymentRequest with complex parameters should be created"); + ok(payRequest2, "PaymentRequest with simplest parameters should be created"); + gScript.addMessageListener("check-complete", function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + }); + gScript.sendAsyncMessage("check-multiple-requests"); + }); + } + + function testCrossOriginTopLevelPrincipal() { + return new Promise((resolve, reject) => { + var ifrr = document.createElement('iframe'); + + window.addEventListener("message", function(event) { + is(event.data, "successful", + "Expected 'successful', but got '" + event.data + "'"); + gScript.addMessageListener("check-complete", function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + }); + gScript.sendAsyncMessage("check-cross-origin-top-level-principal"); + }); + + ifrr.setAttribute('allow', 'payment'); + ifrr.src = "https://test1.example.com:443/tests/dom/payments/test/simple_payment_request.html"; + document.body.appendChild(ifrr); + }); + } + + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler) + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testWithSimplestParameters() + .then(testWithComplexParameters) + .then(testWithNonBasicCardMethods) + .then(testWithDuplicateShippingOptionsParameters) + .then(testMultipleRequests) + .then(testCrossOriginTopLevelPrincipal) + .then(testShippingOptionAttribute) + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345361">Mozilla Bug 1345361</a> +</body> +</html> diff --git a/dom/payments/test/test_currency_amount_validation.html b/dom/payments/test/test_currency_amount_validation.html new file mode 100644 index 0000000000..bf8284f37a --- /dev/null +++ b/dom/payments/test/test_currency_amount_validation.html @@ -0,0 +1,353 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1367669 +https://bugzilla.mozilla.org/show_bug.cgi?id=1388661 +--> +<title>Test for PaymentRequest API currency amount validation</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +const gUrl = SimpleTest.getTestFileURL( + "CurrencyAmountValidationChromeScript.js" +); +const gScript = SpecialPowers.loadChromeScript(gUrl); + +function testFailHandler(message) { + ok(false, message); +} +gScript.addMessageListener("test-fail", testFailHandler); + +const defaultMethods = [ + { + supportedMethods: "basic-card", + }, +]; +const defaultDetails = { + total: { + label: "total", + amount: { + currency: "usd", + value: "1.00", + }, + }, +}; + +const specialAmountDetails = { + total: { + label: "total", + amount: { + currency: "usd", + value: { + toString() { + throw "42"; + }, + }, + }, + }, +}; + +const wellFormedCurrencyCodes = [ + "BOB", + "EUR", + "usd", // currency codes are case-insensitive + "XdR", + "xTs", +]; + +const invalidCurrencyCodes = [ + "", + "€", + "$", + "SFr.", + "DM", + "KR₩", + "702", + "ßP", + "ınr", + "invalid", + "in", + "123", +]; + +const updatedInvalidCurrencyDetails = { + total: { + label: "Total", + amount: { + currency: "Invalid", + value: "1.00", + }, + }, +}; + +const updatedInvalidAmountDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "-1.00", + }, + }, +}; + +const invalidAmounts = [ + "-", + "notdigits", + "ALSONOTDIGITS", + "10.", + ".99", + "-10.", + "-.99", + "10-", + "1-0", + "1.0.0", + "1/3", + "", + null, + " 1.0 ", + " 1.0 ", + "1.0 ", + "USD$1.0", + "$1.0", + { + toString() { + return " 1.0"; + }, + }, + undefined, +]; +const invalidTotalAmounts = invalidAmounts.concat([ + "-1", + "-1.0", + "-1.00", + "-1000.000", +]); + +function updateWithInvalidAmount() { + return new Promise((resolve, reject) => { + resolve(updatedInvalidAmountDetails); + }); +} + +async function testWithLowerCaseCurrency() { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + return new Promise(resolve => { + gScript.addMessageListener( + "check-complete", + function checkCompleteHandler() { + gScript.removeMessageListener("check-complete", checkCompleteHandler); + resolve(); + } + ); + gScript.sendAsyncMessage("check-lower-case-currency"); + }); +} + +function testWithWellFormedCurrencyCodes() { + for (const currency of wellFormedCurrencyCodes) { + const details = { + total: { + label: "Well Formed Currency", + amount: { + currency, + value: "1.00", + }, + }, + }; + try { + const payRequest = new PaymentRequest(defaultMethods, details); + } catch (e) { + const msg = `Unexpected error while creating payment request with well-formed currency (${currency}) ${ + e.name + }`; + ok(false, msg); + } + } +} + +function testWithInvalidCurrencyCodes() { + for (const invalidCurrency of invalidCurrencyCodes) { + const invalidDetails = { + total: { + label: "Invalid Currency", + amount: { + currency: invalidCurrency, + value: "1.00", + }, + }, + }; + try { + const payRequest = new PaymentRequest(defaultMethods, invalidDetails); + ok( + false, + `Creating a Payment Request with invalid currency (${invalidCurrency}) must throw.` + ); + } catch (e) { + is( + e.name, + "RangeError", + `Expected rejected with 'RangeError', but got '${e.name}'.` + ); + } + } +} + +async function testUpdateWithInvalidCurrency() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput( + true + ); + gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service"); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.addEventListener("shippingaddresschange", event => { + event.updateWith(Promise.resolve(updatedInvalidCurrencyDetails)); + }); + payRequest.addEventListener("shippingoptionchange", event => { + event.updateWith(updatedInvalidCurrencyDetails); + }); + try { + await payRequest.show(); + ok(false, "Should have rejected with 'RangeError'"); + } catch (err) { + is( + err.name, + "RangeError", + `Should be rejected with 'RangeError', but got '${err.name}'.` + ); + } + handler.destruct(); +} + +async function testUpdateWithInvalidAmount() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput( + true + ); + gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service"); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.addEventListener("shippingaddresschange", event => { + event.updateWith(updateWithInvalidAmount()); + }); + payRequest.addEventListener("shippingoptionchange", event => { + event.updateWith(updateWithInvalidAmount()); + }); + try { + await payRequest.show(); + ok(false, "Should be rejected with 'TypeError'"); + } catch (err) { + is( + err.name, + "TypeError", + `Should be rejected with 'TypeError', but got ${err.name}.` + ); + } + handler.destruct(); +} + +function testSpecialAmount() { + try { + new PaymentRequest(defaultMethods, specialAmountDetails); + ok(false, "Should throw '42', but got resolved."); + } catch (e) { + is(e, "42", "Expected throw '42'. but got " + e); + } +} + +function testInvalidTotalAmounts() { + for (const invalidAmount of invalidTotalAmounts) { + try { + const invalidDetails = { + total: { + label: "", + amount: { + currency: "USD", + value: invalidAmount, + }, + }, + }; + new PaymentRequest(defaultMethods, invalidDetails); + ok(false, "Should throw 'TypeError', but got resolved."); + } catch (err) { + is(err.name, "TypeError", `Expected 'TypeError', but got '${err.name}'`); + } + } +} + +function testInvalidAmounts() { + for (const invalidAmount of invalidAmounts) { + try { + new PaymentRequest(defaultMethods, { + total: { + label: "", + amount: { + currency: "USD", + value: "1.00", + }, + }, + displayItems: [ + { + label: "", + amount: { + currency: "USD", + value: invalidAmount, + }, + }, + ], + }); + ok(false, "Should throw 'TypeError', but got resolved."); + } catch (err) { + is(err.name, "TypeError", `Expected 'TypeError', but got '${err.name}'.`); + } + } +} + +function teardown() { + return new Promise(resolve => { + gScript.addMessageListener( + "teardown-complete", + function teardownCompleteHandler() { + gScript.removeMessageListener( + "teardown-complete", + teardownCompleteHandler + ); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.destroy(); + SimpleTest.finish(); + resolve(); + } + ); + gScript.sendAsyncMessage("teardown"); + }); +} + +async function runTests() { + try { + testInvalidTotalAmounts(); + testSpecialAmount(); + testInvalidAmounts(); + testWithWellFormedCurrencyCodes(); + testWithInvalidCurrencyCodes(); + await testUpdateWithInvalidAmount(); + await testUpdateWithInvalidCurrency(); + await testWithLowerCaseCurrency(); + await teardown(); + } catch (e) { + console.error(e); + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + } +} + +window.addEventListener("load", () => { + SpecialPowers.pushPrefEnv( + { + set: [["dom.payments.request.enabled", true]], + }, + runTests + ); +}); +</script> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1367669">Mozilla Bug 1367669</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1388661">Mozilla Bug 1388661</a> diff --git a/dom/payments/test/test_payerDetails.html b/dom/payments/test/test_payerDetails.html new file mode 100644 index 0000000000..9a241803af --- /dev/null +++ b/dom/payments/test/test_payerDetails.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> +<title>Test for PaymentResponse.prototype.onpayerdetailchange</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="./DefaultData.js"></script> +<script> + SimpleTest.waitForExplicitFinish(); + + const gUrl = SimpleTest.getTestFileURL("PayerDetailsChromeScript.js"); + const gScript = SpecialPowers.loadChromeScript(gUrl); + + function okTester(result) { + return message => ok(result, message); + } + const passListener = okTester(true); + const failListener = okTester(false); + + gScript.addMessageListener("test-fail", failListener); + gScript.addMessageListener("test-pass", passListener); + + function sendOnce(message) { + return data => { + return new Promise(resolve => { + const doneMsg = `${message}-complete`; + gScript.addMessageListener(doneMsg, function listener() { + gScript.removeMessageListener(doneMsg, listener); + resolve(); + }); + gScript.sendAsyncMessage(message, data); + }); + }; + } + const sendTearDown = sendOnce("teardown"); + + async function loopTest(iterations) { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput( + true + ); + const options = { + requestPayerName: true, + requestPayerEmail: true, + requestPayerPhone: true, + } + const request = new PaymentRequest(defaultMethods, defaultDetails, options); + const response = await request.show(); + is(response.payerName, "", ".payerName must initially be ''"); + is(response.payerEmail, "", ".payerEmail must initially be ''"); + is(response.payerPhone, "", ".payerPhone must initially be ''"); + for (let i = 0; i < iterations; i++) { + const payer = { + name: `test name ${i}`, + phone: `test phone ${i}`, + email: `test email ${i}`, + } + + // Capture the event to firing + const eventPromise = new Promise(resolve => { + response.onpayerdetailchange = resolve; + }); + const retryPromise = response.retry({ + error: "retry-fire-payerdetaichangeevent", + payer + }); + const event = await eventPromise; + + // Check things got updated + is(response.payerName, payer.name, `.payerName must be "${payer.name}"`); + is(response.payerEmail, payer.email, `.payerEmail must be "${payer.email}"`); + is(response.payerPhone, payer.phone, `.payerPhone must be "${payer.phone}"`); + + // Finally, let's do an updateWith() + event.updateWith({ error: "update-with", payerErrors: payer, ...defaultDetails }); + + await retryPromise; + } + + await response.complete("success"); + handler.destruct(); + } + + async function teardown() { + await sendTearDown(); + gScript.removeMessageListener("test-fail", failListener); + gScript.removeMessageListener("test-pass", passListener); + gScript.destroy(); + SimpleTest.finish(); + } + + async function runTests() { + try { + await loopTest(5); // lets go around 5 times + } catch (err) { + ok(false, `Unexpected error: ${err}.`); + } finally { + await teardown(); + } + } + + window.addEventListener("load", () => { + const prefs = [["dom.payments.request.enabled", true]]; + SpecialPowers.pushPrefEnv({ set: prefs }, runTests); + }); +</script> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1472026">Mozilla Bug 1472026</a> diff --git a/dom/payments/test/test_payment-request-in-iframe.html b/dom/payments/test/test_payment-request-in-iframe.html new file mode 100644 index 0000000000..0a4b690f9b --- /dev/null +++ b/dom/payments/test/test_payment-request-in-iframe.html @@ -0,0 +1,168 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1318988 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1318988</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + function testRequestInSameOrigin() { + return new Promise((resolve, reject) => { + var ifr = document.createElement('iframe'); + + let listener = function(event) { + is(event.data, "successful", + "Expected 'successful', but got '" + event.data + "'"); + resolve(); + }; + + window.addEventListener("message", listener); + + ifr.src = "simple_payment_request.html"; + document.body.appendChild(ifr); + + ifr.addEventListener('load', function() { + window.removeEventListener("message", listener); + }); + }); + } + + function testRequestInIFrame() { + return new Promise((resolve, reject) => { + var ifr = document.createElement('iframe'); + + let listener = function(event) { + is(event.data, "SecurityError", + "Expected 'SecurityError', but got '" + event.data + "'"); + resolve(); + }; + + window.addEventListener("message", listener); + + ifr.src = "https://test1.example.com:443/tests/dom/payments/test/simple_payment_request.html"; + document.body.appendChild(ifr); + + ifr.addEventListener('load', function() { + window.removeEventListener("message", listener); + }); + }); + } + + function testRequestInIFrameWithAttribute() { + return new Promise((resolve, reject) => { + var ifrr = document.createElement('iframe'); + + let listener = function(event) { + is(event.data, "successful", + "Expected 'successful', but got '" + event.data + "'"); + resolve(); + }; + + window.addEventListener("message", listener); + + ifrr.setAttribute('allow', 'payment'); + ifrr.src = "https://test1.example.com:443/tests/dom/payments/test/simple_payment_request.html"; + document.body.appendChild(ifrr); + + ifrr.addEventListener('load', function() { + window.removeEventListener("message", listener); + }); + }); + } + + function testRequestWithAttributeChanged() { + return new Promise((resolve, reject) => { + var ifrr = document.createElement('iframe'); + + let i = 0; + + ifrr.addEventListener('load', function() { + if (i === 0) { + ifrr.removeAttribute("allow"); + } + ifrr.contentWindow.postMessage('new PaymentRequest', '*'); + }); + + let listener = function(event) { + i++; + if (i === 1) { + is(event.data, "successful", + "Expected successful when running with allow=payment attribute."); + ifrr.contentWindow.location.href = ifrr.src; + } else { + is(event.data, "SecurityError", + "Expected SecurityError when running without allow=payment attribute."); + window.removeEventListener("message", listener); + resolve(); + } + } + window.addEventListener("message", listener); + + ifrr.setAttribute("allow", "payment"); + ifrr.src = "https://test1.example.com:443/tests/dom/payments/test/echo_payment_request.html"; + + document.body.appendChild(ifrr); + }); + } + + function testRequestInCrossOriginNestedIFrame() { + return new Promise((resolve, reject) => { + var ifrr = document.createElement('iframe'); + + let listener = function(event) { + if (ifrr.allow =! 'payment') { + is(event.data, "SecurityError", + "Expected 'SecurityError' without allow=payment in nested iframe"); + ifrr.setAttribute('allow', "payment"); + ifrr.contentWindow.location.href = ifrr.src; + } else { + is(event.data, "successful", + "Expected 'successful' with allow='payment' in nested iframe"); + window.removeEventListener("message", listener); + resolve(); + } + }; + window.addEventListener("message", listener); + + ifrr.addEventListener("load", function() { + ifrr.contentWindow.postMessage('new PaymentRequest in a new iframe', '*'); + }) + + ifrr.src = "https://test1.example.com:443/tests/dom/payments/test/echo_payment_request.html"; + document.body.appendChild(ifrr); + }); + } + + function runTests() { + testRequestInSameOrigin() + .then(testRequestInIFrame) + .then(testRequestInIFrameWithAttribute) + .then(testRequestWithAttributeChanged) + .then(testRequestInCrossOriginNestedIFrame) + .then(SimpleTest.finish) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1318988">Mozilla Bug 1318988</a> +</body> +</html> diff --git a/dom/payments/test/test_pmi_validation.html b/dom/payments/test/test_pmi_validation.html new file mode 100644 index 0000000000..00d5c0771c --- /dev/null +++ b/dom/payments/test/test_pmi_validation.html @@ -0,0 +1,245 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1389418 +--> +<head> + <meta charset="utf-8"> + <title>Test for PaymentRequest API payment method identifier validation</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('PMIValidationChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + + const defaultMethods = [{ + supportedMethods: "basic-card", + }]; + + const defaultDetails = { + total: { + label: "total", + amount: { + currency: "usd", + value: "1.00", + }, + }, + }; + + const validPMIs = [ + "https://wpt", + "https://wpt.fyi/", + "https://wpt.fyi/payment", + "https://wpt.fyi/payment-request", + "https://wpt.fyi/payment-request?", + "https://wpt.fyi/payment-request?this=is", + "https://wpt.fyi/payment-request?this=is&totally", + "https://wpt.fyi:443/payment-request?this=is&totally", + "https://wpt.fyi:443/payment-request?this=is&totally#fine", + "https://:@wpt.fyi:443/payment-request?this=is&totally#👍", + " \thttps://wpt\n ", + "https://xn--c1yn36f", + "https://點看", + "e", + "n6jzof05mk2g4lhxr-u-q-w1-c-i-pa-ty-bdvs9-ho-ae7-p-md8-s-wq3-h-qd-e-q-sa", + "a-b-q-n-s-pw0", + "m-u", + "s-l5", + "k9-f", + "m-l", + "u4-n-t", + "i488jh6-g18-fck-yb-v7-i", + "x-x-t-t-c34-o", + "basic-card", + ]; + + const invalidPMIs = [ + "https://:password@example.com", + "https://username@example.com", + "https://username:password@example.com/pay", + "http://username:password@example.com/pay", + "https://:@example.com:100000000/pay", + "https://foo.com:100000000/pay", + "basic-💳", + "not-https://wpt.fyi/payment-request", + "../realitive/url", + "/absolute/../path?", + "https://", + "¡basic-*-card!", + "Basic-Card", + "0", + "-", + "--", + "a--b", + "-a--b", + "a-b-", + "0-", + "0-a", + "a0--", + "A-", + "A-B", + "A-b", + "a-0", + "a-0b", + " a-b", + "\t\na-b", + "a-b ", + "a-b\n\t", + ]; + + function testWithValidPMIs() { + return new Promise((resolve, reject) => { + for (const validPMI of validPMIs) { + try { + const validMethods = [{supportedMethods: validPMI},]; + const payRequest = new PaymentRequest(validMethods, defaultDetails); + resolve(); + } catch (e) { + ok(false, "Unexpected error '" + e.name + "'."); + resolve(); + } + } + }); + } + + function testWithInvalidPMIs() { + return new Promise((resolve, reject) => { + for (const invalidPMI of invalidPMIs) { + try { + const invalidMethods = [{supportedMethods: invalidPMI},]; + const payRequest = new PaymentRequest(invalidMethods, defaultDetails); + ok(false, "Expected throw 'RangeError', but got resolved"); + resolve(); + } catch (e) { + is(e.name, "RangeError", "Expected 'RangeError'."); + resolve(); + } + } + }); + } + + function testUpdateWithValidPMI() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + + gScript.sendAsyncMessage("set-ui-service"); + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.addEventListener("shippingoptionchange", event => { + const validDetails = { + total: { + label: "total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + modifiers: [{ + supportedMethods: "https://example.com", + total: { + label: "total", + amount: { + currency: "USD", + value: "1.00", + }, + } + },], + } + event.updateWith(validDetails); + }); + payRequest.show().then((response) => { + response.complete("success").then(() => { + resolve(); + }).catch((e) => { + ok(false, "Unexpected error '" + e.name + "'."); + resolve(); + }); + }).catch((e) => { + ok(false, "Unexpected error '" + e.name + "'."); + resolve(); + }).finally(handler.destruct); + }); + } + + function testUpdateWithInvalidPMI() { + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + + gScript.sendAsyncMessage("set-ui-service"); + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails); + payRequest.addEventListener("shippingoptionchange", event => { + const invalidDetails = { + total: { + label: "total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + modifiers: [{ + supportedMethods: "https://username:password@example.com", + total: { + label: "total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + },], + } + event.updateWith(invalidDetails); + }); + payRequest.show().then((result) => { + ok(false, "Expected throw 'RangeError', but got resolved."); + resolve(); + }).catch((e) => { + is(e.name, "RangeError", "Expected 'RangeError'."); + resolve(); + }).finally(handler.destruct); + }); + } + + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler) + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testWithValidPMIs() + .then(testWithInvalidPMIs) + .then(testUpdateWithValidPMI) + .then(testUpdateWithInvalidPMI) + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1389418">Mozilla Bug 1389418</a> +</body> +</html> diff --git a/dom/payments/test/test_requestShipping.html b/dom/payments/test/test_requestShipping.html new file mode 100644 index 0000000000..b866588953 --- /dev/null +++ b/dom/payments/test/test_requestShipping.html @@ -0,0 +1,180 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1436903 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1436903</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('RequestShippingChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + + const defaultMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ['unionpay', 'visa', 'mastercard', 'amex', 'discover', + 'diners', 'jcb', 'mir', + ], + }, + }, { + supportedMethods: "testing-payment-method", + }]; + const defaultDetails = { + id: "test payment", + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: false, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + }; + + const defaultOptions = { + requestPayerName: true, + requestPayerEmail: false, + requestPayerPhone: false, + requestShipping: false, + shippingType: "shipping" + }; + + const updatedOptionDetails = { + total: { + label: "Total", + amount: { + currency: "USD", + value: "1.00" + } + }, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: false, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: true, + }, + ], + }; + + const nonSupportedMethods = [{ + supportedMethods: "nonsupported-method", + }]; + + + function updateWithShippingAddress() { + return new Promise((resolve, reject) => { + resolve(defaultDetails); + }); + } + + function updateWithShippingOption() { + return new Promise((resolve, reject) => { + resolve(updatedOptionDetails); + }); + } + + function testShow() { + gScript.sendAsyncMessage("set-normal-ui-service"); + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + + payRequest.addEventListener("shippingaddresschange", event => { + event.updateWith(updateWithShippingAddress()); + }); + payRequest.addEventListener("shippingoptionchange", event => { + event.updateWith(updateWithShippingOption()); + }); + + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + payRequest.show().then(response => { + response.complete("success").then(() =>{ + resolve(); + }).catch(e => { + ok(false, "Unexpected error: " + e.name); + resolve(); + }); + }).catch( e => { + ok(false, "Unexpected error: " + e.name); + resolve(); + }).finally(handler.destruct); + }); + } + + function teardown() { + ok(true, "Mandatory assert"); + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler) + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testShow() + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1436903">Mozilla Bug 1436903</a> +</body> +</html> diff --git a/dom/payments/test/test_retryPayment.html b/dom/payments/test/test_retryPayment.html new file mode 100644 index 0000000000..3ce389f475 --- /dev/null +++ b/dom/payments/test/test_retryPayment.html @@ -0,0 +1,354 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1435161 +--> +<head> + <meta charset="utf-8"> + <title>Test for retry PaymentRequest</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + const gUrl = SimpleTest.getTestFileURL('RetryPaymentChromeScript.js'); + const gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + gScript.sendAsyncMessage(action, params); + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + }); + } + + const validationErrors = { + error: "error", + shippingAddress: { + addressLine: "addressLine error", + city: "city error", + country: "country error", + dependentLocality: "dependentLocality error", + organization: "organization error", + phone: "phone error", + postalCode: "postalCode error", + recipient: "recipient error", + region: "region error", + regionCode: "regionCode error", + sortingCode: "sortingCode error", + }, + payer: { + name: "name error", + email: "email error", + phone: "phone error", + }, + paymentMethod: { + account: "method account error", + password: "method password error", + }, + }; + + const options = { + requestPayerName: true, + requestPayerEmail: true, + requestPayerPhone: true, + requestShipping: true, + shippingType: "shipping" + }; + + function checkShowResponse(testName, payResponse) { + const { payerName, payerEmail, payerPhone } = payResponse.toJSON(); + is( + payerName, + "Bill A. Pacheco", + `${testName}: Expected 'Bill A. Pacheco' on payerName, but got '${payerName}' after show PaymentRequest` + ); + is( + payerEmail, + "", + `${testName}: Expected '' on payerEmail, but got '${payerEmail}' after show PaymentRequest` + ); + is( + payerPhone, + "", + `${testName}: Expected '' on payerPhone, but got '${payerPhone}' after show PaymentRequest` + ); + } + + function checkRetryResponse(testName, payResponse) { + const { payerName, payerEmail, payerPhone } = payResponse.toJSON(); + is( + payerName, + "Bill A. Pacheco", + `${testName}: Expected 'Bill A. Pacheco' on payerName, but got '${payerName}' after retry PaymentRequest` + ); + is( + payerEmail, + "bpacheco@test.org", + `${testName} : Expected 'bpacheco@test.org' on payerEmail, but got '${payerEmail}' after retry PaymentRequest` + ); + is( + payerPhone, + "+123456789", + `${testName} : Expected '+123456789' on payerPhone, but got '${payerPhone}' after retry PaymentRequest` + ); + } + + function unexpectedErrMsg(testName, errName, timing) { + return `${testName}: Unexpected error(${errName}) when ${timing} the PaymentRequest.`; + } + + function expectedErrMsg(testName, expectedErr, errName, timing) { + return `${testName}: Expected '${expectedErr}' when ${timing} PaymentResponse, but got '${errName}'.`; + } + + async function testRetryAfterComplete() { + const testName = "testRetryAfterComplete"; + await requestChromeAction("start-test", testName); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, options); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + await requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + await checkShowResponse(testName, payResponse); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.Name, "showing")); + await requestChromeAction("finish-test"); + handler.destruct(); + return; + } + try { + await payResponse.complete("success"); + } catch(err) { + let errName = err.Name; + ok(false, unexpectedErrMsg(testName, err.Name, "completing")); + await requestChromeAction("finish-test"); + return; + } + try { + await payResponse.retry(validationErrors); + ok(false, `${testName}: Unexpected success when retry the PaymentResponse.`); + return; + } catch(err) { + is(err.name, + "InvalidStateError", + expectedErrMsg(testName, "InvalidStateError", err.name, "retrying")); + await requestChromeAction("finish-test"); + return; + } + await requestChromeAction("finish-test"); + } + + async function testRetryAfterRetry() { + const testName = "testRetryAfterRetry"; + await requestChromeAction("start-test", testName); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, options); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + await requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + await checkShowResponse(testName, payResponse); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "showing")); + await requestChromeAction("finish-test"); + handler.destruct(); + return; + } + let retryPromise; + try { + retryPromise = payResponse.retry(validationErrors); + await payResponse.retry(validationErrors); + ok(false, `${testName}: Unexpected success when retry the PaymentResponse.`); + await requestChromeAction("finish-test"); + return; + } catch(err) { + is(err.name, + "InvalidStateError", + expectedErrMsg(testName, "InvalidStateError", err.name, "retrying")); + } + try { + await retryPromise; + await payResponse.complete("success"); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "completing")); + await requestChromeAction("finish-test"); + return; + } + await requestChromeAction("finish-test"); + } + + async function testRetryWithEmptyErrors() { + const testName = "testRetryWithEmptyErrors"; + await requestChromeAction("start-test", testName); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, options); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + await checkShowResponse(testName, payResponse); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "showing")); + await requestChromeAction("finish-test"); + handler.destruct(); + return; + } + try { + await payResponse.retry(); + ok(false, `${testName}: Unexpected success when retry the PaymentResponse.`); + await requestChromeAction("finish-test"); + return; + } catch(err) { + is(err.name, + "AbortError", + expectedErrMsg(testName, "AbortError", err.name, "retrying")); + } + try { + await payResponse.complete("success"); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "completing")); + await requestChromeAction("finish-test"); + return; + } + await requestChromeAction("finish-test"); + } + + async function testRetry() { + const testName = "testRetry"; + await requestChromeAction("start-test", testName); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, options); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + await requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + await checkShowResponse(testName, payResponse); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "showing")); + await requestChromeAction("finish-test"); + handler.destruct(); + return; + } + try { + await payResponse.retry(validationErrors); + await checkRetryResponse(testName, payResponse); + await payResponse.complete("success"); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "retrying")); + await requestChromeAction("finish-test"); + return; + } + await requestChromeAction("finish-test"); + } + + async function testRetryAbortByUser() { + const testName = "testRetryAbortByUser"; + await requestChromeAction("reject-retry"); + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, options); + ok(payRequest, testName + ": failed to create PaymentRequest."); + if (!payRequest) { + await requestChromeAction("finish-test"); + return; + } + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let payResponse; + try { + payResponse = await payRequest.show(); + await checkShowResponse(testName, payResponse); + handler.destruct(); + } catch(err) { + ok(false, unexpectedErrMsg(testName, err.name, "showing")); + handler.destruct(); + await requestChromeAction("finish-test"); + return; + } + try { + await payResponse.retry(validationErrors); + ok(false, `${testName}: Unexpected success when retry the PaymentResponse.`); + await requestChromeAction("finish-test"); + return; + } catch(err) { + is(err.name, + "AbortError", + expectedErrMsg(testName, "AbortError", err.name, "retrying")); + } + await requestChromeAction("finish-test"); + } + + function teardown() { + return new Promise((resolve, reject) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function runTests() { + try { + await testRetryAfterComplete() + await testRetryAfterRetry() + await testRetryWithEmptyErrors() + await testRetry() + await testRetryAbortByUser() + await teardown() + } catch(e) { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + } + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1435161">Mozilla Bug 1435161</a> +</body> +</html> diff --git a/dom/payments/test/test_shippingOptions.html b/dom/payments/test/test_shippingOptions.html new file mode 100644 index 0000000000..887ec30de5 --- /dev/null +++ b/dom/payments/test/test_shippingOptions.html @@ -0,0 +1,208 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1440041 +https://bugzilla.mozilla.org/show_bug.cgi?id=1443914 +--> +<head> + <meta charset="utf-8"> + <title>Test for shippingOptions related bugs</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="./DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('ShippingOptionsChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + let shippingOptions = [{ + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00", + }, + selected: true, + },{ + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "5.00", + }, + selected: false, + }] + + // testing function main body + function testShippingOptionsTemplate(initDetails, + optionUpdateDetails, + expectedRequestOption, + expectedOptionChangeOption, + expectedResponseOption) { + const expectedResults = {requestResult: expectedRequestOption, + changeOptionResult: expectedOptionChangeOption, + responseResult: expectedResponseOption,}; + gScript.sendAsyncMessage("set-expected-results", expectedResults); + return new Promise((resolve, reject) => { + const request = new PaymentRequest(defaultMethods, initDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + is(request.shippingOption, expectedRequestOption, + "request.shippingOption should be " + expectedRequestOption + + " after created, but got " + request.shippingOption + "."); + if (optionUpdateDetails) { + request.addEventListener("shippingoptionchange", event => { + is(request.shippingOption, expectedOptionChangeOption, + "request.shippingOption should be " + expectedOptionChangeOption + + " in shippingoptionchange event, but got " + request.shippingOption + "."); + event.updateWith(optionUpdateDetails); + }); + } + request.show().then(response => { + is(response.shippingOption, expectedResponseOption, + "response.shippingOption should be " + expectedResponseOption + + ", but got " + response.shippingOption + "."); + response.complete("success").then(() => { + resolve(); + }).catch(error => { + ok(false, "Unexpected error: " + error.name); + resolve(); + }) + }, response => { + }).catch(error => { + ok(false, "Unexpected error: " + error.name); + resolve(); + }).finally(handler.destruct); + }); + } + + // test no selected shipping option in default + function testNoSelectedShippingOptions() { + return testShippingOptionsTemplate(defaultDetails, // initial details + null, // update details for optionchange + null, // expected request.shippintOption after create + null, // expected request.shippingOption after optionchange + null); // expected response.shippingOption + } + + // test select one shipping option in default + function testSelectedOneShippingOption() { + let details = Object.assign({}, defaultDetails); + details.shippingOptions = shippingOptions; + details.shippingOptions[0].selected = true; + details.shippingOptions[1].selected = false; + const expectedOption = details.shippingOptions[0].id; + return testShippingOptionsTemplate(details, // initial details + null, // update details for optionchange + expectedOption, // expected request.shippintOption after create + null, // expected request.shippingOption after optionchange + expectedOption); // expected response.shippingOption + } + + // test select multiple shipping options in default + function testMultiSelectedShippingOptions() { + let details = Object.assign({}, defaultDetails); + details.shippingOptions = shippingOptions; + details.shippingOptions[0].selected = true; + details.shippingOptions[1].selected = true; + const expectedOption = details.shippingOptions[1].id; + return testShippingOptionsTemplate(details, // initial details + null, // update details for optionchange + expectedOption, // expected request.shippintOption after create + null, // expected request.shippingOption after optionchange + expectedOption); // expected response.shippingOption + } + + // test no selected shipping option in default, but selected by user + function testSelectedByUser() { + let updateDetails = Object.assign({}, defaultDetails); + updateDetails.shippingOptions = shippingOptions; + updateDetails.shippingOptions[0].selected = true; + updateDetails.shippingOptions[1].selected = false; + const expectedOption = updateDetails.shippingOptions[0].id; + return testShippingOptionsTemplate(defaultDetails, // initial details + updateDetails, // update details for optionchange + null, // expected request.shippintOption after create + expectedOption, // expected request.shippingOption after optionchange + expectedOption); // expected response.shippingOption + } + + // test no selected shipping option in default, but selected by user then updated + // by merchant to the other. + function testUpdateSelectedByMerchant() { + let updateDetails = Object.assign({}, defaultDetails); + updateDetails.shippingOptions = shippingOptions; + updateDetails.shippingOptions[0].selected = false; + updateDetails.shippingOptions[1].selected = true; + const expectedOption = updateDetails.shippingOptions[0].id; + const expectedResponse = updateDetails.shippingOptions[1].id; + return testShippingOptionsTemplate(defaultDetails, // initial details + updateDetails, // update details for optionchange + null, // expected request.shippintOption after create + expectedOption, // expected request.shippingOption after optionchange + expectedResponse);// expected response.shippingOption + } + + // test update shipping options to null + function testUpdateShippingOptionsToNull() { + let updateDetails = Object.assign({}, defaultDetails); + delete updateDetails.shippingOptions; + const expectedOption = defaultDetails.shippingOptions[0].id; + return testShippingOptionsTemplate(defaultDetails, // initial details + updateDetails, // update details for optionchange + null, // expected request.shippintOption after create + expectedOption, // expected request.shippingOption after optionchange + null); // expected response.shippingOption + } + + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + function runTests() { + testNoSelectedShippingOptions() + .then(testSelectedOneShippingOption) + .then(testMultiSelectedShippingOptions) + .then(testSelectedByUser) + .then(testUpdateSelectedByMerchant) + .then(testUpdateShippingOptionsToNull) + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1440041">Mozilla Bug 1440041</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1443914">Mozilla Bug 1443914</a> +</body> +</html> diff --git a/dom/payments/test/test_showPayment.html b/dom/payments/test/test_showPayment.html new file mode 100644 index 0000000000..2a4a0bb4f7 --- /dev/null +++ b/dom/payments/test/test_showPayment.html @@ -0,0 +1,504 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1345366 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1345366</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('ShowPaymentChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + async function requestChromeAction(action, params) { + await new Promise(resolve => { + gScript.addMessageListener(`${action}-complete`, function completeListener() { + gScript.removeMessageListener(`${action}-complete`, completeListener); + resolve(); + }); + gScript.sendAsyncMessage(action, params); + }); + } + + // testing data declaration + // default parameters for PaymentRequest construction + const defaultMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ['unionpay', 'visa', 'mastercard', 'amex', 'discover', + 'diners', 'jcb', 'mir', + ], + }, + }, { + supportedMethods: "testing-payment-method", + }]; + + const defaultTotal = { + label: "Total", + amount: { + currency: "USD", + value: "1.00", + }, + } + + const defaultDetails = { + id: "test payment", + total: defaultTotal, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: false, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: false, + }, + ], + }; + + const defaultOptions = { + requestPayerName: true, + requestPayerEmail: false, + requestPayerPhone: false, + requestShipping: true, + shippingType: "shipping" + }; + + // testing data for PaymentRequestUpdateEvent.updateWith() + const updatedShippingOptionsDetails = { + total: defaultTotal, + shippingOptions: [ + { + id: "NormalShipping", + label: "NormalShipping", + amount: { + currency: "USD", + value: "10.00" + }, + selected: false, + }, + { + id: "FastShipping", + label: "FastShipping", + amount: { + currency: "USD", + value: "30.00" + }, + selected: true, + }, + ], + }; + + const updatedErrorDetails = { + total: defaultTotal, + error: "Update with Error", + }; + + // Promise function for PaymentRequestUpdateEvent.updateWith() + function updateWithPromise(detailsUpdate) { + return new Promise((resolve, reject) => { + if (detailsUpdate) { + resolve(detailsUpdate); + } else { + reject(); + } + }); + } + + // testing data for PaymentRequest.show() with Non-supported methods + const nonSupportedMethods = [{ + supportedMethods: "nonsupported-method", + }]; + + + // checking functions + function checkAddress(testName, address, fromEvent) { + is(address.country, + "USA", + `${testName}: address.country should be 'USA'.`); + is(address.region, + "CA", + `${testName}: address.region should be 'CA'.`); + is(address.city, + "San Bruno", + `${testName}: address.city should be 'San Bruno'.`); + is(address.dependentLocality, + "Test locality", + `${testName}: address.dependentLocality should be 'Test locality'.`); + is(address.postalCode, + "94066", + `${testName}: address.postalCode should be '94066'.`); + is(address.sortingCode, + "123456", + `${testName}: address.sortingCode should be '123456'.`); + if (fromEvent) { + is(address.addressLine.length, + 0, + `${testName}: address.addressLine.length should be 0 from event.`); + is(address.organization, + "", + `${testName}: address.organization should be empty from event.`); + is(address.recipient, + "", + `${testName}: address.recipient should be empty from event.`); + is(address.phone, + "", + `${testName}: address.phone should be empty from event.`); + } else { + is(address.addressLine.length, + 1, + `${testName}: address.addressLine.length should be 1 from promise.`); + is(address.addressLine[0], + "Easton Ave", + `${testName}: address.addressLine[0] should be 'Easton Ave' from promise.`); + is(address.organization, + "Testing Org", + `${testName}: address.organization should be 'Testing Org' from promise.`); + is(address.recipient, + "Bill A. Pacheco", + `${testName}: address.recipient should be 'Bill A. Pacheco' from promise.`); + is(address.phone, + "+1-434-441-3879", + `${testName}: address.phone should be '+1-434-441-3879' from promise.`); + } + } + + function checkResponse(testName, response) { + is(response.requestId, + "test payment", + `${testName}: response.requestId should be 'test payment'.`); + is(response.methodName, + "testing-payment-method", + `${testName}: response.methodName should be 'testing-payment-method'.`); + is(response.details.paymentToken, + "6880281f-0df3-4b8e-916f-66575e2457c1", + `${testName}: response.details.paymentToken should be '6880281f-0df3-4b8e-916f-66575e2457c1'.`); + checkAddress(testName, response.shippingAddress, false/*fromEvent*/); + is(response.shippingOption, + "FastShipping", + `${testName}: response.shippingOption should be 'FastShipping'.`); + is(response.payerName, + "Bill A. Pacheco", + `${testName}: response.payerName should be 'Bill A. Pacheco'.`); + ok(!response.payerEmail, + `${testName}: response.payerEmail should be empty`); + ok(!response.payerPhone, + `${testName}: response.payerPhone should be empty`); + } + + // testing functions + async function testShowNormalFlow() { + const testName = "testShowNormalFlow"; + await requestChromeAction("set-normal-ui-service", testName); + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + request.addEventListener("shippingaddresschange", event => { + checkAddress(testName, request.shippingAddress, true/*fromEvent*/); + event.updateWith(updateWithPromise(defaultDetails)); + }); + request.addEventListener("shippingoptionchange", event => { + event.updateWith(updateWithPromise(updatedShippingOptionsDetails)); + }); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + let response = await request.show(); + checkResponse(testName, response, false); + await response.complete(); + } catch (error) { + ok(false, `${testName} Unexpected error: ${e.name}`); + } + await handler.destruct(); + } + + // testing show with nonsupported methods + async function testCannotMakePaymentShow() { + const testName = "testCannotMakePaymentShow"; + await requestChromeAction("set-simple-ui-service", testName); + + const request = new PaymentRequest(nonSupportedMethods, defaultDetails); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let result = await request.canMakePayment(); + ok(!result, `${testName}: canMakePayment() should return false.`); + try { + await request.show(); + ok(false, `${testName}: should be rejected with 'NotSupportedError' but got resolved.`); + } catch (error) { + is(error.name, "NotSupportedError", `${testName}: should be rejected with 'NotSupportedError'.`); + } + await handler.destruct(); + } + + // testing show rejected by user + async function testRejectShow() { + const testName = "testRejectShow"; + await requestChromeAction("set-reject-ui-service", testName); + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + await request.show(); + ok(false, `${testName}: Should be rejected with 'AbortError' but got resolved.`); + } catch(error) { + is(error.name, "AbortError", `${testName}: Should be rejected with 'AbortError'.`); + } + await handler.destruct(); + } + + // testing PaymentResponse.complete() with specified result + async function testCompleteStatus(testName, result) { + await requestChromeAction("set-simple-ui-service", testName); + if (result) { + await requestChromeAction(`set-complete-status-${result}`); + } else { + await requestChromeAction(`set-complete-status-unknown`); + } + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + let response = await request.show(); + await response.complete(result); + } catch (error) { + ok(false, `${testName}: Unexpected error ${error.name}.`); + } + await handler.destruct(); + } + + async function testCompleteFail() { + const testName = "testCompleteFail"; + return testCompleteStatus(testName, "fail"); + } + + async function testCompleteSuccess() { + const testName = "testCompleteSuccess"; + return testCompleteStatus(testName, "success"); + } + + async function testCompleteUnknown() { + const testName = "testCompleteUnknown" + return testCompleteStatus(testName, "unknown"); + } + + async function testCompleteEmpty() { + const testName = "testCompleteEmpty"; + return testCompleteStatus(testName); + } + + // testing PaymentRequestUpdateEvent.updateWith with specified details and error + async function testUpdateWith(testName, detailsUpdate, expectedError) { + if (expectedError) { + await requestChromeAction("set-update-with-error-ui-service", testName); + } else { + await requestChromeAction("set-update-with-ui-service", testName); + } + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + request.addEventListener("shippingaddresschange", event => { + event.updateWith(updateWithPromise(detailsUpdate)); + }); + request.addEventListener("shippingoptionchange", event => { + event.updateWith(updateWithPromise(detailsUpdate)); + }); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + const response = await request.show(); + if (expectedError) { + ok(false, `${testName}: Should be rejected with ${expectedError} but got resolved.`); + } else { + await response.complete("success"); + } + } catch(error) { + if (expectedError) { + is(error.name, expectedError, `${testName}: Should be rejected with ${expectedError}.`); + } else { + ok(false, `${testName}: Unexpected error ${error.name}.`); + } + } + await handler.destruct(); + } + + async function testUpdateWithReject() { + const testName = "testUpdateWithReject"; + return testUpdateWith(testName, null, "AbortError"); + } + + async function testUpdateWithValidDetails() { + const testName = "testUpdateWithValidDetails"; + return testUpdateWith(testName, updatedShippingOptionsDetails, null); + } + + async function testUpdateWithInvalidDetails() { + const testName = "testUpdateWithInvalidDetails"; + return testUpdateWith(testName, {total: "invalid details"}, "TypeError"); + } + + async function testUpdateWithError() { + const testName = "testUpdateWithError"; + return testUpdateWith(testName, updatedErrorDetails, "AbortError"); + } + + // testing show with detailsUpdate promise + async function testShowWithDetailsPromise(testName, detailsUpdate, expectedError) { + if (expectedError) { + await requestChromeAction("set-reject-ui-service", testName); + } else { + await requestChromeAction("set-simple-ui-service", testName); + } + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + ok(!request.shippingOption, `${testName}: request.shippingOption should be null.`); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + try { + let response = await request.show(updateWithPromise(detailsUpdate)); + if (expectedError) { + ok(false, `${testName}: Should be rejected with ${expectedError} but got resolved.`); + } else { + ok(response.shippingOption, + `${testName}: response.shippingOption should not be null.`); + } + await response.complete(); + } catch(error) { + if (expectedError) { + is(error.name, expectedError, `${testName}: Should be rejected with ${expectedError}.`); + } else { + ok(false, `${testName}: Unexpected error ${error.name}.`); + } + } + await handler.destruct(); + } + async function testShowWithValidPromise() { + const testName = "testShowWithValidPromise"; + return testShowWithDetailsPromise(testName, updatedShippingOptionsDetails, null); + } + + async function testShowWithRejectedPromise() { + const testName = "testShowWithRejectedPromise"; + return testShowWithDetailsPromise(testName, null, "AbortError"); + } + + async function testShowWithInvalidPromise() { + const testName = "testShowWithInvalidPromise"; + return testShowWithDetailsPromise(testName, {total: "invalid details"}, "TypeError"); + } + + async function testShowWithErrorPromise() { + const testName = "testShowWithErrorPromise"; + return testShowWithDetailsPromise(testName, updatedErrorDetails, "AbortError"); + } + + async function testShowWithPromiseResolvedByRejectedPromise() { + const testName = "testShowWithPromiseResolvedByRejectedPromise"; + await requestChromeAction("set-reject-ui-service", testName); + + const request = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + let rejectPromise = Promise.reject(new TypeError()); + let detailsUpdatePromise = Promise.resolve(rejectPromise); + try { + await request.show(detailsUpdatePromise); + ok(false, `${testName}: should be rejected with AbortError but got resolved.`); + } catch(error) { + is(error.name, "AbortError", `${testName}: should be rejected with AbortError.`); + } + await handler.destruct(); + } + + // testing show response initialization in chrome process + async function testShowResponseInit() { + const testName = "testShowResponseInit"; + await requestChromeAction("test-show-response-init", testName); + } + + // testing show that is not triggered by user. + async function testShowNotTriggeredByUser() { + const testName = "testShowNotTriggeredByUser"; + await requestChromeAction("set-simple-ui-service", testName); + + const request = new PaymentRequest(defaultMethods, defaultDetails); + try { + await request.show(); + ok(false, `${testName}: should be rejected with SecurityError, but got resolved.`); + } catch (error) { + is(error.name, "SecurityError", `${testName}: should be rejected with SecurityError.`); + } + } + + // teardown function + async function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + // test main body + async function runTests() { + try { + await testCannotMakePaymentShow(); + await testRejectShow(); + await testShowNormalFlow(); + await testCompleteSuccess(); + await testCompleteFail(); + await testCompleteUnknown(); + await testCompleteEmpty(); + await testUpdateWithReject(); + await testUpdateWithValidDetails(); + await testUpdateWithInvalidDetails(); + await testUpdateWithError(); + await testShowWithValidPromise(); + await testShowWithInvalidPromise(); + await testShowWithRejectedPromise(); + await testShowWithErrorPromise(); + await testShowWithPromiseResolvedByRejectedPromise(); + await testShowResponseInit(); + await testShowNotTriggeredByUser(); + await teardown(); + } catch (error) { + ok(false, `test_showPayment: Unexpected error: ${error.name}`); + SimpleTest.finish(); + } + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1345366">Mozilla Bug 1345366</a> +</body> +</html> diff --git a/dom/payments/test/test_update_errors.html b/dom/payments/test/test_update_errors.html new file mode 100644 index 0000000000..a473cf2706 --- /dev/null +++ b/dom/payments/test/test_update_errors.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1435157 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1435157</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="DefaultData.js"></script> + <script type="application/javascript"> + + "use strict"; + SimpleTest.waitForExplicitFinish(); + + var gUrl = SimpleTest.getTestFileURL('UpdateErrorsChromeScript.js'); + var gScript = SpecialPowers.loadChromeScript(gUrl); + + function testFailHandler(message) { + ok(false, message); + } + function testPassHandler(message) { + ok(true, message); + } + gScript.addMessageListener("test-fail", testFailHandler); + gScript.addMessageListener("test-pass", testPassHandler); + + const addressErrors = { + addressLine: "addressLine error", + city: "city error", + country: "country error", + dependentLocality: "dependentLocality error", + organization: "organization error", + phone: "phone error", + postalCode: "postalCode error", + recipient: "recipient error", + region: "region error", + regionCode: "regionCode error", + sortingCode: "sortingCode error", + }; + + const payErrors = { + email: "email error", + name: "name error", + phone: "phone error", + }; + + let updateDetails = { + total:{ + label: "Total", + amount: { + currency: "USD", + value: "1.00", + }, + }, + erros: "shipping address error", + shippingAddressErrors: addressErrors, + payerErrors: payErrors, + } + + // testing functions + function testUpdateErrors() { + return new Promise((resolve, reject) => { + const payRequest = new PaymentRequest(defaultMethods, defaultDetails, defaultOptions); + payRequest.addEventListener("shippingaddresschange", event => { + event.updateWith(updateDetails); + }); + payRequest.addEventListener("shippingoptionchange", event => { + event.updateWith(updatedDetails); + }); + const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true); + payRequest.show().then(response => { + ok(false, "Expected AbortError, but got pass"); + resolve(); + }, error => { + is(error.name, "AbortError", "Expect AbortError, but got " + error.name); + resolve(); + }).catch( e => { + ok(false, "Unexpected error: " + e.name); + resolve(); + }).finally(handler.destruct); + }); + } + + // teardown function + function teardown() { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.removeMessageListener("test-fail", testFailHandler); + gScript.removeMessageListener("test-pass", testPassHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("teardown"); + } + + // test main body + function runTests() { + testUpdateErrors() + .then(teardown) + .catch( e => { + ok(false, "Unexpected error: " + e.name); + SimpleTest.finish(); + }); + } + + window.addEventListener('load', function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.payments.request.enabled', true], + ] + }, runTests); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1435157">Mozilla Bug 1435157</a> +</body> +</html> |