summaryrefslogtreecommitdiffstats
path: root/dom/payments
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/payments/BasicCardPayment.cpp121
-rw-r--r--dom/payments/BasicCardPayment.h37
-rw-r--r--dom/payments/MerchantValidationEvent.cpp190
-rw-r--r--dom/payments/MerchantValidationEvent.h76
-rw-r--r--dom/payments/PaymentActionResponse.cpp421
-rw-r--r--dom/payments/PaymentActionResponse.h192
-rw-r--r--dom/payments/PaymentAddress.cpp87
-rw-r--r--dom/payments/PaymentAddress.h76
-rw-r--r--dom/payments/PaymentMethodChangeEvent.cpp165
-rw-r--r--dom/payments/PaymentMethodChangeEvent.h56
-rw-r--r--dom/payments/PaymentRequest.cpp1261
-rw-r--r--dom/payments/PaymentRequest.h280
-rw-r--r--dom/payments/PaymentRequestData.cpp809
-rw-r--r--dom/payments/PaymentRequestData.h257
-rw-r--r--dom/payments/PaymentRequestManager.cpp742
-rw-r--r--dom/payments/PaymentRequestManager.h101
-rw-r--r--dom/payments/PaymentRequestService.cpp606
-rw-r--r--dom/payments/PaymentRequestService.h61
-rw-r--r--dom/payments/PaymentRequestUpdateEvent.cpp161
-rw-r--r--dom/payments/PaymentRequestUpdateEvent.h62
-rw-r--r--dom/payments/PaymentRequestUtils.cpp60
-rw-r--r--dom/payments/PaymentRequestUtils.h31
-rw-r--r--dom/payments/PaymentResponse.cpp458
-rw-r--r--dom/payments/PaymentResponse.h180
-rw-r--r--dom/payments/components.conf80
-rw-r--r--dom/payments/ipc/PPaymentRequest.ipdl256
-rw-r--r--dom/payments/ipc/PaymentRequestChild.cpp140
-rw-r--r--dom/payments/ipc/PaymentRequestChild.h57
-rw-r--r--dom/payments/ipc/PaymentRequestParent.cpp468
-rw-r--r--dom/payments/ipc/PaymentRequestParent.h58
-rw-r--r--dom/payments/ipc/moz.build26
-rw-r--r--dom/payments/moz.build53
-rw-r--r--dom/payments/test/BasicCardErrorsChromeScript.js133
-rw-r--r--dom/payments/test/BasiccardChromeScript.js372
-rw-r--r--dom/payments/test/Bug1478740ChromeScript.js90
-rw-r--r--dom/payments/test/Bug1490698ChromeScript.js226
-rw-r--r--dom/payments/test/ClosePaymentChromeScript.js160
-rw-r--r--dom/payments/test/ConstructorChromeScript.js490
-rw-r--r--dom/payments/test/CurrencyAmountValidationChromeScript.js67
-rw-r--r--dom/payments/test/DefaultData.js59
-rw-r--r--dom/payments/test/GeneralChromeScript.js20
-rw-r--r--dom/payments/test/PMIValidationChromeScript.js82
-rw-r--r--dom/payments/test/PayerDetailsChromeScript.js83
-rw-r--r--dom/payments/test/RequestShippingChromeScript.js116
-rw-r--r--dom/payments/test/RetryPaymentChromeScript.js238
-rw-r--r--dom/payments/test/ShippingOptionsChromeScript.js120
-rw-r--r--dom/payments/test/ShowPaymentChromeScript.js393
-rw-r--r--dom/payments/test/UpdateErrorsChromeScript.js214
-rw-r--r--dom/payments/test/blank_page.html16
-rw-r--r--dom/payments/test/browser.ini10
-rw-r--r--dom/payments/test/browser_payment_in_different_tabs.js37
-rw-r--r--dom/payments/test/bug1478740.html44
-rw-r--r--dom/payments/test/echo_payment_request.html37
-rw-r--r--dom/payments/test/head.js127
-rw-r--r--dom/payments/test/mochitest.ini53
-rw-r--r--dom/payments/test/simple_payment_request.html81
-rw-r--r--dom/payments/test/test_abortPayment.html95
-rw-r--r--dom/payments/test/test_basiccard.html371
-rw-r--r--dom/payments/test/test_basiccarderrors.html85
-rw-r--r--dom/payments/test/test_block_none10s.html58
-rw-r--r--dom/payments/test/test_bug1478740.html140
-rw-r--r--dom/payments/test/test_bug1490698.html119
-rw-r--r--dom/payments/test/test_canMakePayment.html164
-rw-r--r--dom/payments/test/test_closePayment.html284
-rw-r--r--dom/payments/test/test_constructor.html351
-rw-r--r--dom/payments/test/test_currency_amount_validation.html353
-rw-r--r--dom/payments/test/test_payerDetails.html107
-rw-r--r--dom/payments/test/test_payment-request-in-iframe.html168
-rw-r--r--dom/payments/test/test_pmi_validation.html245
-rw-r--r--dom/payments/test/test_requestShipping.html180
-rw-r--r--dom/payments/test/test_retryPayment.html354
-rw-r--r--dom/payments/test/test_shippingOptions.html208
-rw-r--r--dom/payments/test/test_showPayment.html504
-rw-r--r--dom/payments/test/test_update_errors.html121
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>