[Payment Request] Implement PaymentResponse.retry()
authoraestes@apple.com <aestes@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 30 Oct 2018 19:07:40 +0000 (19:07 +0000)
committeraestes@apple.com <aestes@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 30 Oct 2018 19:07:40 +0000 (19:07 +0000)
https://bugs.webkit.org/show_bug.cgi?id=190985

Source/WebCore:

Reviewed by Daniel Bates.

Implemented the retry() method on PaymentResponse as specified in the Payment Request API
W3C Editor's Draft of 24 October 2018.

See https://w3c.github.io/payment-request/#retry-method for details.

Tests: http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html
       http/tests/paymentrequest/payment-response-retry-method.https.html

* Modules/applepay/PaymentCoordinator.h:
(WebCore::PaymentCoordinator::client): Added. Returns m_client.
* Modules/applepay/PaymentCoordinatorClient.h:
(WebCore::PaymentCoordinatorClient::isMockPaymentCoordinator const): Added. Used to downcast
a PaymentCoordinatorClient to a MockPaymentCoordinator.
* Modules/applepay/paymentrequest/ApplePayPaymentHandler.cpp:
(WebCore::ApplePayPaymentHandler::computeTotalAndLineItems const): Made const.
(WebCore::ApplePayPaymentHandler::computeErrors const): Broke this function into
computeAddressErrors, computePayerErrors, and computePaymentMethodErrors, then modified this
function to call those functions. Exceptions thrown by computePaymentMethodErrors are ignored.
(WebCore::ApplePayPaymentHandler::computeAddressErrors const): Added.
(WebCore::ApplePayPaymentHandler::computePayerErrors const): Added.
(WebCore::ApplePayPaymentHandler::computePaymentMethodErrors const): Added.
(WebCore::ApplePayPaymentHandler::complete): Added ASSERTs to verify whether result is a
final result.
(WebCore::ApplePayPaymentHandler::retry): Computed PaymentErrors from PaymentValidationErrors,
ensured the PaymentAuthorizationResult was non-final by adding an unknown error if necessary,
and called PaymentCoordinator::completePaymentSession.
* Modules/applepay/paymentrequest/ApplePayPaymentHandler.h:
* Modules/paymentrequest/PaymentHandler.h:
* Modules/paymentrequest/PaymentRequest.cpp:
(WebCore::PaymentRequest::show): Changed to call settleShowPromise.
(WebCore::PaymentRequest::abortWithException): Changed to abort PaymentResponse's retry
promise, if present, instead of PaymentResponse's show promise.
(WebCore::PaymentRequest::settleShowPromise): Added. Settles m_showPromise then sets it to
std::nullopt.
(WebCore::PaymentRequest::closeActivePaymentHandler): Added. Hides the active payment
handler then sets it to std::nullopt.
(WebCore::PaymentRequest::stop): Stopped calling abortWithException, since that function
might settle PaymentResponse's retry promise. PaymentResponse is now an ActiveDOMObject and
will settle its own promise in its implementation of stop.
(WebCore::PaymentRequest::abort): Changed to throw an InvalidStateError if there is a
pending retry promise and to call abortWithException instead of stop.
(WebCore::PaymentRequest::completeMerchantValidation): Changed to call abortWithException
instead of stop.
(WebCore::PaymentRequest::settleDetailsPromise): Ditto.
(WebCore::PaymentRequest::accept): Updated the existing PaymentResponse, if present, rather
than creating a new one. Settled the existing PaymentResponse's retry promise, if present,
rather than the show promise.
(WebCore::PaymentRequest::complete): Changed to throw an AbortError if there is no longer an
active payment handler.
(WebCore::PaymentRequest::retry): Changed to throw an AbortError if there is no longer an
active payment handler, and to call PaymentHandler::retry if there is.
(WebCore::PaymentRequest::cancel): Changed to call abortWithException instead of stop.
* Modules/paymentrequest/PaymentRequest.h:
* Modules/paymentrequest/PaymentRequest.idl:
* Modules/paymentrequest/PaymentResponse.cpp:
(WebCore::PaymentResponse::PaymentResponse):
(WebCore::PaymentResponse::finishConstruction): Pending activities create strong references
to |this|, so they cannot be created in constructors without relaxing adoption requirements.
Added this function so that the pending activity can be created after the PaymentResponse is
created and adopted.
(WebCore::PaymentResponse::~PaymentResponse):
(WebCore::PaymentResponse::complete): Updated to throw an AbortError or InvalidStateError
when necessary.
(WebCore::PaymentResponse::retry): Implemented. Throws an AbortError or InvalidStateError
when necessary, otherwise calls PaymentRequest::retry and stores the retry promise in
m_retryPromise.
(WebCore::PaymentResponse::abortWithException): Added. Rejects the retry promise with
|exception|, clears the pending activity, and sets m_state to Completed.
(WebCore::PaymentResponse::settleRetryPromise): Added. Settles the retry promise and sets it
to std::nullopt.
(WebCore::PaymentResponse::canSuspendForDocumentSuspension const): Added. Returns true if
there is no pending activity.
(WebCore::PaymentResponse::stop): Added. Rejects the retry promise with AbortError, clears
the pending activity, and sets m_state to Stopped.
* Modules/paymentrequest/PaymentResponse.h: Changed create to call finishConstruction and
made PaymentResponse an ActiveDOMObject instead of a ContextDestructionObserver.
* testing/Internals.cpp:
(WebCore::Internals::Internals): Changed to only create a MockPaymentCoordinator for main frames.
(WebCore::Internals::mockPaymentCoordinator): Changed to get the MockPaymentCoordinator by
downcasting the page's payment coordinator client.
* testing/Internals.h:
* testing/Internals.idl:
* testing/MockPaymentCoordinator.cpp:
(WebCore::MockPaymentCoordinator::completePaymentSession): Changed to only increment
hideCount for final results.
* testing/MockPaymentCoordinator.h:
(isType): Added so that PaymentCoordinatorClients can be downcasted to MockPaymentCoordinators.

LayoutTests:

Reviewed by Daniel Bates

* http/tests/paymentrequest/payment-address-attributes-and-toJSON-method.https.html:
* http/tests/paymentrequest/payment-response-complete-method.https.html:
* http/tests/paymentrequest/payment-response-methodName-attribute.https.html:
* http/tests/paymentrequest/payment-response-payerEmail-attribute.https.html:
* http/tests/paymentrequest/payment-response-payerName-attribute.https.html:
* http/tests/paymentrequest/payment-response-payerPhone-attribute.https.html:
* http/tests/paymentrequest/payment-response-rejects-if-not-active.https-expected.txt: Added.
* http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html: Copied from
imported/w3c/web-platform-tests/payment-request/payment-response/rejects_if_not_active-manual.https.html
and automated using internals.mockPaymentCoordinator.
* http/tests/paymentrequest/payment-response-retry-method.https-expected.txt: Added.
* http/tests/paymentrequest/payment-response-retry-method.https.html: Copied from
imported/w3c/web-platform-tests/payment-request/payment-response/retry-method-manual.https.html
and automated using internals.mockPaymentCoordinator.
* http/tests/paymentrequest/resources/helpers.js:
(setUpAndSmokeTest):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@237597 268f45cc-cd09-0410-ab3c-d52691b4dbfc

28 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/paymentrequest/payment-address-attributes-and-toJSON-method.https.html
LayoutTests/http/tests/paymentrequest/payment-response-complete-method.https.html
LayoutTests/http/tests/paymentrequest/payment-response-methodName-attribute.https.html
LayoutTests/http/tests/paymentrequest/payment-response-payerEmail-attribute.https.html
LayoutTests/http/tests/paymentrequest/payment-response-payerName-attribute.https.html
LayoutTests/http/tests/paymentrequest/payment-response-payerPhone-attribute.https.html
LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html [new file with mode: 0644]
LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https.html [new file with mode: 0644]
LayoutTests/http/tests/paymentrequest/resources/helpers.js
Source/WebCore/ChangeLog
Source/WebCore/Modules/applepay/PaymentCoordinator.h
Source/WebCore/Modules/applepay/PaymentCoordinatorClient.h
Source/WebCore/Modules/applepay/paymentrequest/ApplePayPaymentHandler.cpp
Source/WebCore/Modules/applepay/paymentrequest/ApplePayPaymentHandler.h
Source/WebCore/Modules/paymentrequest/PaymentHandler.h
Source/WebCore/Modules/paymentrequest/PaymentRequest.cpp
Source/WebCore/Modules/paymentrequest/PaymentRequest.h
Source/WebCore/Modules/paymentrequest/PaymentRequest.idl
Source/WebCore/Modules/paymentrequest/PaymentResponse.cpp
Source/WebCore/Modules/paymentrequest/PaymentResponse.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebCore/testing/MockPaymentCoordinator.cpp
Source/WebCore/testing/MockPaymentCoordinator.h

index 0482c89..abdb3b2 100644 (file)
@@ -1,5 +1,29 @@
 2018-10-30  Andy Estes  <aestes@apple.com>
 
+        [Payment Request] Implement PaymentResponse.retry()
+        https://bugs.webkit.org/show_bug.cgi?id=190985
+
+        Reviewed by Daniel Bates
+
+        * http/tests/paymentrequest/payment-address-attributes-and-toJSON-method.https.html:
+        * http/tests/paymentrequest/payment-response-complete-method.https.html:
+        * http/tests/paymentrequest/payment-response-methodName-attribute.https.html:
+        * http/tests/paymentrequest/payment-response-payerEmail-attribute.https.html:
+        * http/tests/paymentrequest/payment-response-payerName-attribute.https.html:
+        * http/tests/paymentrequest/payment-response-payerPhone-attribute.https.html:
+        * http/tests/paymentrequest/payment-response-rejects-if-not-active.https-expected.txt: Added.
+        * http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html: Copied from
+        imported/w3c/web-platform-tests/payment-request/payment-response/rejects_if_not_active-manual.https.html
+        and automated using internals.mockPaymentCoordinator.
+        * http/tests/paymentrequest/payment-response-retry-method.https-expected.txt: Added.
+        * http/tests/paymentrequest/payment-response-retry-method.https.html: Copied from
+        imported/w3c/web-platform-tests/payment-request/payment-response/retry-method-manual.https.html
+        and automated using internals.mockPaymentCoordinator.
+        * http/tests/paymentrequest/resources/helpers.js:
+        (setUpAndSmokeTest):
+
+2018-10-30  Andy Estes  <aestes@apple.com>
+
         [Apple Pay] PaymentRequest.canMakePayment() should resolve to true whenever Apple Pay is available
         https://bugs.webkit.org/show_bug.cgi?id=191039
 
index 6f8293a..70a433f 100644 (file)
@@ -10,6 +10,7 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
 <script>
+setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
 const options = { requestShipping: true };
 function runTest(button, expected = {}) {
   button.disabled = true;
index 5e689ec..4b4d614 100644 (file)
@@ -10,6 +10,7 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
 <script>
+setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
 async function runTest({ completeWith: result }, button) {
   button.disabled = true;
   const { response, request } = await getPaymentRequestResponse();
index 13c6fdd..851f4ed 100644 (file)
@@ -9,6 +9,9 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
+<script>
+       setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
+</script>
 <ol>
   <li>
     <button id="button">
index 4da935c..97c5e9d 100644 (file)
@@ -9,6 +9,9 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
+<script>
+  setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
+</script>
 <ol>
   <li>
     <button id="button1">
index fd1793a..6c5695a 100644 (file)
@@ -9,6 +9,9 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
+<script>
+  setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
+</script>
 <ol>
   <li>
     <button id="button1">
index cbdcf6c..a06c10f 100644 (file)
@@ -9,6 +9,9 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/helpers.js"></script>
+<script>
+  setUpAndSmokeTest({ explicit_done: true, explicit_timeout: true });
+</script>
 <ol>
   <li>
     <button id="button1">
diff --git a/LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https-expected.txt b/LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https-expected.txt
new file mode 100644 (file)
index 0000000..c3ffb3c
--- /dev/null
@@ -0,0 +1,5 @@
+
+PASS retry()'s retryPromise rejects if document is not fully active. 
+PASS retry()'s retryPromise rejects if the document becomes not fully active. 
+PASS complete()'s completePromise rejects if document is not fully active. 
+
diff --git a/LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html b/LayoutTests/http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html
new file mode 100644 (file)
index 0000000..32d101f
--- /dev/null
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="help" href="https://w3c.github.io/payment-request/#retry-method">
+<title>PaymentResponse retry() rejects if doc is not fully active</title>
+<script src="/js-test-resources/ui-helper.js"></script>
+<script src="/resources/payment-request.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-retry">
+<body>
+<script>
+setup({ explicit_done: true, explicit_timeout: true });
+const validMethods = Object.freeze([validPaymentMethod()]);
+const validAmount = Object.freeze({
+  currency: "USD",
+  value: "5.00",
+});
+const validTotal = Object.freeze({
+  label: "Total due",
+  amount: validAmount,
+});
+const validDetails = Object.freeze({
+  total: validTotal,
+});
+
+function getLoadedPaymentResponse(iframe, url) {
+  return new Promise(resolve => {
+    iframe.addEventListener(
+      "load",
+      () => {
+        const { PaymentRequest } = iframe.contentWindow;
+        activateThen(async () => {
+          const request = new PaymentRequest(validMethods, validDetails);
+          const acceptPromise = request.show();
+          internals.mockPaymentCoordinator.acceptPayment();
+          const response = await acceptPromise;
+          resolve(response);
+        });
+      },
+      { once: true }
+    );
+    iframe.src = url;
+  });
+}
+
+function methodNotFullyActive(text, methodName, ...args) {
+  promise_test(async t => {
+    const iframe = document.createElement("iframe");
+    iframe.allowPaymentRequest = true;
+    document.body.appendChild(iframe);
+
+    // We first got to page1.html, grab a PaymentResponse instance.
+    const response = await getLoadedPaymentResponse(
+      iframe,
+      "/payment-request/resources/page1.html"
+    );
+    // We navigate the iframe again, putting response's document into an inactive state.
+    await new Promise(resolve => {
+      iframe.addEventListener("load", resolve);
+      iframe.src = "/payment-request/resources/page2.html";
+    });
+    // Now, response's relevant global object's document is no longer active.
+    // So, promise needs to reject appropriately.
+    const promise = response[methodName](...args);
+    await promise_rejects(
+      t,
+      "AbortError",
+      promise,
+      "Inactive document, so must throw AbortError"
+    );
+    // We are done, so clean up.
+    iframe.remove();
+  }, text);
+}
+
+function methodBecomesNotFullyActive(text, methodName, ...args) {
+  promise_test(async t => {
+    const iframe = document.createElement("iframe");
+    iframe.allowPaymentRequest = true;
+    document.body.appendChild(iframe);
+
+    // We first got to page1.html, grab a PaymentResponse instance.
+    const response = await getLoadedPaymentResponse(
+      iframe,
+      "/payment-request/resources/page1.html"
+    );
+
+    // we get the promise from page1.html, while it's active!
+    const promise = response[methodName](...args);
+
+    // We navigate the iframe again, putting response's document into an inactive state.
+    await new Promise(resolve => {
+      iframe.addEventListener("load", resolve);
+      iframe.src = "/payment-request/resources/page2.html";
+    });
+
+    // Now, response's relevant global object's document is no longer active.
+    // So, promise needs to reject appropriately.
+    await promise_rejects(
+      t,
+      "AbortError",
+      promise,
+      "Inactive document, so must throw AbortError"
+    );
+    // We are done, so clean up.
+    iframe.remove();
+  }, text);
+}
+
+window.addEventListener("load", async () => {
+  await methodNotFullyActive("retry()'s retryPromise rejects if document is not fully active.", 'retry', {});
+  await methodBecomesNotFullyActive("retry()'s retryPromise rejects if the document becomes not fully active.", 'retry', {});
+  await methodNotFullyActive("complete()'s completePromise rejects if document is not fully active.", 'complete', 'success');
+  done();
+});
+</script>
diff --git a/LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https-expected.txt b/LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https-expected.txt
new file mode 100644 (file)
index 0000000..605f3d5
--- /dev/null
@@ -0,0 +1,13 @@
+
+PASS Can construct a payment request (smoke test). 
+PASS PaymentResponse.prototype must have a retry() function (smoke test). 
+PASS A completed payment request cannot be retried. 
+PASS Calling retry() more than once yield a rejected promise, but the retryPromise resolves independently. 
+PASS Calling complete() while a retry() is in progress results in an InvalidStateError. 
+PASS Trying to abort() via PaymentRequest is not possible. 
+PASS It's possible to retry() multiple times and eventually complete(). After complete(), however, retry() rejects with an InvalidStateError. 
+PASS The user aborting retrying a payment causes the retryPromise to reject with AbortError. Aborting a payment is causes it complete. 
+PASS When retrying, the user is shown error fields to fix. 
+PASS When "abort the update" occurs because of an update error, the `retryPromise` is rejected and response.[[complete]] becomes true. 
+PASS Calling retry() multiple times is always a new object. 
+
diff --git a/LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https.html b/LayoutTests/http/tests/paymentrequest/payment-response-retry-method.https.html
new file mode 100644 (file)
index 0000000..6abb5bf
--- /dev/null
@@ -0,0 +1,188 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-retry">
+<title>
+  PaymentResponse.prototype.retry() method
+</title>
+<script src="/js-test-resources/ui-helper.js"></script>
+<script src="/resources/payment-request.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+  setUpAndSmokeTest({ explicit_timeout: true });
+</script>
+<body>
+<script>
+test(() => {
+  assert_true(
+    "retry" in PaymentResponse.prototype,
+    "retry must be in prototype"
+  );
+  assert_true(
+    PaymentResponse.prototype.retry instanceof Function,
+    "retry must be a function"
+  );
+}, "PaymentResponse.prototype must have a retry() function (smoke test).");
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  // sets response.[[complete]] to true.
+  await response.complete("success");
+  return promise_rejects(
+    t,
+    "InvalidStateError",
+    response.retry({}),
+    "response.[[complete]] is true, so rejects with InvalidStateError."
+  );
+}, "A completed payment request cannot be retried.");
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  const retryPromise = response.retry({});
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.retry({}),
+    "Calling retry() again rejects with an InvalidStateError"
+  );
+  internals.mockPaymentCoordinator.acceptPayment();
+  await retryPromise;
+  await response.complete("success");
+}, "Calling retry() more than once yield a rejected promise, but the retryPromise resolves independently.");
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  const retryPromise = response.retry({});
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.complete("success"),
+    "Calling complete() while retrying rejects with an InvalidStateError"
+  );
+  internals.mockPaymentCoordinator.acceptPayment();
+  await retryPromise;
+  await response.complete("success");
+}, "Calling complete() while a retry() is in progress results in an InvalidStateError.");
+
+promise_test(async t => {
+  const { response, request } = await getPaymentRequestResponse();
+  const retryPromise = response.retry({});
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    request.abort(),
+    "Calling request.abort() while retrying rejects with an InvalidStateError"
+  );
+  internals.mockPaymentCoordinator.acceptPayment();
+  await retryPromise;
+  await response.complete("success");
+}, "Trying to abort() via PaymentRequest is not possible.");
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  var promise = response.retry({});
+  internals.mockPaymentCoordinator.acceptPayment();
+  assert_equals(
+    await promise,
+    undefined,
+    "Expected undefined as the resolve value"
+  );
+  promise = response.retry({});
+  internals.mockPaymentCoordinator.acceptPayment();
+  assert_equals(
+    await promise,
+    undefined,
+    "Expected undefined as the resolve value"
+  );
+  await response.complete("success");
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.retry({}),
+    "Calling retry() after complete() rejects with a InvalidStateError"
+  );
+}, "It's possible to retry() multiple times and eventually complete(). After complete(), however, retry() rejects with an InvalidStateError.");
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  var promise = response.retry({});
+  internals.mockPaymentCoordinator.cancelPayment();
+  await promise_rejects(
+    t,
+    "AbortError",
+    promise,
+    "The user aborting a retry rejects with a AbortError"
+  );
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.retry({}),
+    "After the user aborts, response [[complete]] is true so retry() must reject with InvalidStateError"
+  );
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.complete("success"),
+    "After the user aborts, response [[complete]] is true, so complete() rejects with a InvalidStateError"
+  );
+}, "The user aborting retrying a payment causes the retryPromise to reject with AbortError. Aborting a payment is causes it complete.");
+
+promise_test(async t => {
+  const { response, request } = await getPaymentRequestResponse({ requestShipping: true });
+  const retryPromise = response.retry({
+    shippingAddress: { city: "Invalid city", addressLine: "Invalid address line" },
+  });
+  const errors = internals.mockPaymentCoordinator.errors;
+  assert_equals(errors.length, 2, "Must have two errors");
+  assert_equals(errors[0].code, "shippingContactInvalid", "Must be a 'shippingContactInvalid' error");
+  assert_equals(errors[0].message, "Invalid address line", "Error message must match");
+  assert_equals(errors[0].contactField, "addressLines", "Contact field must match");
+  assert_equals(errors[1].code, "shippingContactInvalid", "Must be a 'shippingContactInvalid' error");
+  assert_equals(errors[1].message, "Invalid city", "Error message must match");
+  assert_equals(errors[1].contactField, "locality", "Contact field must match");
+  internals.mockPaymentCoordinator.acceptPayment();
+  await retryPromise;
+  await response.complete("success");
+}, "When retrying, the user is shown error fields to fix.");
+
+promise_test(async t => {
+  const { response, request } = await getPaymentRequestResponse({ requestShipping: true });
+  // causes "abort the update" to run
+  const shippingChangedPromise = new Promise(resolve => {
+    request.onshippingoptionchange = () => {
+      event.updateWith({ total: { amount: { currency: "USD", value: "INVALID" } }});
+      resolve();
+    };
+  });
+  const retryPromise = response.retry({});
+  internals.mockPaymentCoordinator.changeShippingOption("option");
+  await shippingChangedPromise;
+  await promise_rejects(
+    t,
+    new TypeError(),
+    retryPromise,
+    "retry() aborts with a TypeError, because totals' value is invalid"
+  );
+  await promise_rejects(
+    t,
+    "InvalidStateError",
+    response.complete("success"),
+    "After the user aborts, response [[complete]] is true, so complete() rejects with a InvalidStateError"
+  );
+}, 'When "abort the update" occurs because of an update error, the `retryPromise` is rejected and response.[[complete]] becomes true.');
+
+promise_test(async t => {
+  const { response } = await getPaymentRequestResponse();
+  const retryPromise = response.retry({});
+  const promises = new Set([
+    retryPromise,
+    response.retry({}).catch(() => {}),
+    response.retry({}).catch(() => {}),
+  ]);
+  assert_equals(promises.size, 3, "Must have three unique objects");
+  internals.mockPaymentCoordinator.acceptPayment();
+  await retryPromise;
+  await response.complete();
+}, "Calling retry() multiple times is always a new object.");
+</script>
index 3f71b02..47dfa2c 100644 (file)
@@ -1,5 +1,3 @@
-setup({ explicit_done: true, explicit_timeout: true });
-
 const validMethod = Object.freeze({
   supportedMethods: "https://apple.com/apple-pay",
   data: {
@@ -22,18 +20,23 @@ const validTotal = Object.freeze({
   label: "Valid total",
   amount: validAmount,
 });
+
 const validDetails = {
   total: validTotal,
 };
 
-test(() => {
-  try {
-    new PaymentRequest(validMethods, validDetails);
-  } catch (err) {
-    done();
-    throw err;
-  }
-}, "Can construct a payment request (smoke test).");
+function setUpAndSmokeTest(options)
+{
+  setup(options);
+  test(() => {
+    try {
+      new PaymentRequest(validMethods, validDetails);
+    } catch (err) {
+      done();
+      throw err;
+    }
+  }, "Can construct a payment request (smoke test).");
+}
 
 /**
  * Pops up a payment sheet, allowing options to be
index 09b6ed9..d45e386 100644 (file)
@@ -1,3 +1,98 @@
+2018-10-30  Andy Estes  <aestes@apple.com>
+
+        [Payment Request] Implement PaymentResponse.retry()
+        https://bugs.webkit.org/show_bug.cgi?id=190985
+
+        Reviewed by Daniel Bates.
+
+        Implemented the retry() method on PaymentResponse as specified in the Payment Request API
+        W3C Editor's Draft of 24 October 2018.
+
+        See https://w3c.github.io/payment-request/#retry-method for details.
+
+        Tests: http/tests/paymentrequest/payment-response-rejects-if-not-active.https.html
+               http/tests/paymentrequest/payment-response-retry-method.https.html
+
+        * Modules/applepay/PaymentCoordinator.h:
+        (WebCore::PaymentCoordinator::client): Added. Returns m_client.
+        * Modules/applepay/PaymentCoordinatorClient.h:
+        (WebCore::PaymentCoordinatorClient::isMockPaymentCoordinator const): Added. Used to downcast
+        a PaymentCoordinatorClient to a MockPaymentCoordinator.
+        * Modules/applepay/paymentrequest/ApplePayPaymentHandler.cpp:
+        (WebCore::ApplePayPaymentHandler::computeTotalAndLineItems const): Made const.
+        (WebCore::ApplePayPaymentHandler::computeErrors const): Broke this function into
+        computeAddressErrors, computePayerErrors, and computePaymentMethodErrors, then modified this
+        function to call those functions. Exceptions thrown by computePaymentMethodErrors are ignored.
+        (WebCore::ApplePayPaymentHandler::computeAddressErrors const): Added.
+        (WebCore::ApplePayPaymentHandler::computePayerErrors const): Added.
+        (WebCore::ApplePayPaymentHandler::computePaymentMethodErrors const): Added.
+        (WebCore::ApplePayPaymentHandler::complete): Added ASSERTs to verify whether result is a
+        final result.
+        (WebCore::ApplePayPaymentHandler::retry): Computed PaymentErrors from PaymentValidationErrors,
+        ensured the PaymentAuthorizationResult was non-final by adding an unknown error if necessary,
+        and called PaymentCoordinator::completePaymentSession.
+        * Modules/applepay/paymentrequest/ApplePayPaymentHandler.h:
+        * Modules/paymentrequest/PaymentHandler.h:
+        * Modules/paymentrequest/PaymentRequest.cpp:
+        (WebCore::PaymentRequest::show): Changed to call settleShowPromise.
+        (WebCore::PaymentRequest::abortWithException): Changed to abort PaymentResponse's retry
+        promise, if present, instead of PaymentResponse's show promise.
+        (WebCore::PaymentRequest::settleShowPromise): Added. Settles m_showPromise then sets it to
+        std::nullopt.
+        (WebCore::PaymentRequest::closeActivePaymentHandler): Added. Hides the active payment
+        handler then sets it to std::nullopt.
+        (WebCore::PaymentRequest::stop): Stopped calling abortWithException, since that function
+        might settle PaymentResponse's retry promise. PaymentResponse is now an ActiveDOMObject and
+        will settle its own promise in its implementation of stop.
+        (WebCore::PaymentRequest::abort): Changed to throw an InvalidStateError if there is a
+        pending retry promise and to call abortWithException instead of stop.
+        (WebCore::PaymentRequest::completeMerchantValidation): Changed to call abortWithException
+        instead of stop.
+        (WebCore::PaymentRequest::settleDetailsPromise): Ditto.
+        (WebCore::PaymentRequest::accept): Updated the existing PaymentResponse, if present, rather
+        than creating a new one. Settled the existing PaymentResponse's retry promise, if present,
+        rather than the show promise.
+        (WebCore::PaymentRequest::complete): Changed to throw an AbortError if there is no longer an
+        active payment handler.
+        (WebCore::PaymentRequest::retry): Changed to throw an AbortError if there is no longer an
+        active payment handler, and to call PaymentHandler::retry if there is.
+        (WebCore::PaymentRequest::cancel): Changed to call abortWithException instead of stop.
+        * Modules/paymentrequest/PaymentRequest.h:
+        * Modules/paymentrequest/PaymentRequest.idl:
+        * Modules/paymentrequest/PaymentResponse.cpp:
+        (WebCore::PaymentResponse::PaymentResponse):
+        (WebCore::PaymentResponse::finishConstruction): Pending activities create strong references
+        to |this|, so they cannot be created in constructors without relaxing adoption requirements.
+        Added this function so that the pending activity can be created after the PaymentResponse is
+        created and adopted.
+        (WebCore::PaymentResponse::~PaymentResponse):
+        (WebCore::PaymentResponse::complete): Updated to throw an AbortError or InvalidStateError
+        when necessary.
+        (WebCore::PaymentResponse::retry): Implemented. Throws an AbortError or InvalidStateError
+        when necessary, otherwise calls PaymentRequest::retry and stores the retry promise in
+        m_retryPromise.
+        (WebCore::PaymentResponse::abortWithException): Added. Rejects the retry promise with
+        |exception|, clears the pending activity, and sets m_state to Completed.
+        (WebCore::PaymentResponse::settleRetryPromise): Added. Settles the retry promise and sets it
+        to std::nullopt.
+        (WebCore::PaymentResponse::canSuspendForDocumentSuspension const): Added. Returns true if
+        there is no pending activity.
+        (WebCore::PaymentResponse::stop): Added. Rejects the retry promise with AbortError, clears
+        the pending activity, and sets m_state to Stopped.
+        * Modules/paymentrequest/PaymentResponse.h: Changed create to call finishConstruction and
+        made PaymentResponse an ActiveDOMObject instead of a ContextDestructionObserver.
+        * testing/Internals.cpp:
+        (WebCore::Internals::Internals): Changed to only create a MockPaymentCoordinator for main frames.
+        (WebCore::Internals::mockPaymentCoordinator): Changed to get the MockPaymentCoordinator by
+        downcasting the page's payment coordinator client.
+        * testing/Internals.h:
+        * testing/Internals.idl:
+        * testing/MockPaymentCoordinator.cpp:
+        (WebCore::MockPaymentCoordinator::completePaymentSession): Changed to only increment
+        hideCount for final results.
+        * testing/MockPaymentCoordinator.h:
+        (isType): Added so that PaymentCoordinatorClients can be downcasted to MockPaymentCoordinators.
+
 2018-10-30  Zalan Bujtas  <zalan@apple.com>
 
         [iOS] Use the mainframe view size to compute window.outerWidth/height.
index d4652cf..a6f1b6e 100644 (file)
@@ -50,6 +50,8 @@ public:
     WEBCORE_EXPORT explicit PaymentCoordinator(PaymentCoordinatorClient&);
     WEBCORE_EXPORT ~PaymentCoordinator();
 
+    PaymentCoordinatorClient& client() { return m_client; }
+
     bool supportsVersion(unsigned version) const;
     bool canMakePayments();
     void canMakePaymentsWithActiveCard(const String& merchantIdentifier, const String& domainName, WTF::Function<void (bool)>&& completionHandler);
index e915ca1..b97030a 100644 (file)
@@ -58,6 +58,8 @@ public:
     virtual void cancelPaymentSession() = 0;
     virtual void paymentCoordinatorDestroyed() = 0;
 
+    virtual bool isMockPaymentCoordinator() const { return false; }
+
 protected:
     virtual ~PaymentCoordinatorClient() = default;
 };
index 37387fe..f455664 100644 (file)
@@ -53,6 +53,7 @@
 #include "PaymentMethod.h"
 #include "PaymentRequestValidator.h"
 #include "PaymentResponse.h"
+#include "PaymentValidationErrors.h"
 #include "Settings.h"
 
 namespace WebCore {
@@ -249,7 +250,7 @@ ExceptionOr<Vector<ApplePaySessionPaymentRequest::ShippingMethod>> ApplePayPayme
     return WTFMove(shippingMethods);
 }
 
-ExceptionOr<ApplePaySessionPaymentRequest::TotalAndLineItems> ApplePayPaymentHandler::computeTotalAndLineItems()
+ExceptionOr<ApplePaySessionPaymentRequest::TotalAndLineItems> ApplePayPaymentHandler::computeTotalAndLineItems() const
 {
     auto& details = m_paymentRequest->paymentDetails();
     String currency = details.total.amount.currency;
@@ -322,21 +323,43 @@ Vector<PaymentError> ApplePayPaymentHandler::computeErrors(String&& error, Addre
 {
     Vector<PaymentError> errors;
 
-    auto& options = m_paymentRequest->paymentOptions();
-    using ContactField = PaymentError::ContactField;
+    if (m_paymentRequest->paymentDetails().shippingOptions.isEmpty())
+        computeAddressErrors(WTFMove(error), WTFMove(addressErrors), errors);
+
+    computePayerErrors(WTFMove(payerErrors), errors);
 
-    if (options.requestShipping && m_paymentRequest->paymentDetails().shippingOptions.isEmpty()) {
-        appendShippingContactInvalidError(WTFMove(error), std::nullopt, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.addressLine), ContactField::AddressLines, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.city), ContactField::Locality, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.country), ContactField::Country, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.dependentLocality), ContactField::SubLocality, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.phone), ContactField::PhoneNumber, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.postalCode), ContactField::PostalCode, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.recipient), ContactField::Name, errors);
-        appendShippingContactInvalidError(WTFMove(addressErrors.region), ContactField::AdministrativeArea, errors);
+    auto scope = DECLARE_CATCH_SCOPE(scriptExecutionContext()->vm());
+    auto exception = computePaymentMethodErrors(paymentMethodErrors, errors);
+    if (exception.hasException()) {
+        ASSERT(scope.exception());
+        scope.clearException();
     }
 
+    return errors;
+}
+
+void ApplePayPaymentHandler::computeAddressErrors(String&& error, AddressErrors&& addressErrors, Vector<PaymentError>& errors) const
+{
+    if (!m_paymentRequest->paymentOptions().requestShipping)
+        return;
+
+    using ContactField = PaymentError::ContactField;
+    appendShippingContactInvalidError(WTFMove(error), std::nullopt, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.addressLine), ContactField::AddressLines, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.city), ContactField::Locality, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.country), ContactField::Country, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.dependentLocality), ContactField::SubLocality, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.phone), ContactField::PhoneNumber, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.postalCode), ContactField::PostalCode, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.recipient), ContactField::Name, errors);
+    appendShippingContactInvalidError(WTFMove(addressErrors.region), ContactField::AdministrativeArea, errors);
+}
+
+void ApplePayPaymentHandler::computePayerErrors(PayerErrorFields&& payerErrors, Vector<PaymentError>& errors) const
+{
+    auto& options = m_paymentRequest->paymentOptions();
+    using ContactField = PaymentError::ContactField;
+
     if (options.requestPayerName)
         appendShippingContactInvalidError(WTFMove(payerErrors.name), ContactField::Name, errors);
 
@@ -345,25 +368,29 @@ Vector<PaymentError> ApplePayPaymentHandler::computeErrors(String&& error, Addre
 
     if (options.requestPayerPhone)
         appendShippingContactInvalidError(WTFMove(payerErrors.phone), ContactField::PhoneNumber, errors);
+}
+
+ExceptionOr<void> ApplePayPaymentHandler::computePaymentMethodErrors(JSC::JSObject* paymentMethodErrors, Vector<PaymentError>& errors) const
+{
+    if (!paymentMethodErrors)
+        return { };
 
 #if ENABLE(APPLE_PAY_SESSION_V3)
-    if (paymentMethodErrors) {
-        auto& context = *scriptExecutionContext();
-        auto catchScope = DECLARE_CATCH_SCOPE(context.vm());
-        auto applePayErrors = convert<IDLSequence<IDLInterface<ApplePayError>>>(*context.execState(), paymentMethodErrors);
-        if (!catchScope.exception()) {
-            for (auto& applePayError : applePayErrors) {
-                if (applePayError)
-                    errors.append({ applePayError->code(), applePayError->message(), applePayError->contactField() });
-            }
-        } else
-            catchScope.clearException();
+    auto& context = *scriptExecutionContext();
+    auto throwScope = DECLARE_THROW_SCOPE(context.vm());
+    auto applePayErrors = convert<IDLSequence<IDLInterface<ApplePayError>>>(*context.execState(), paymentMethodErrors);
+    if (throwScope.exception())
+        return Exception { ExistingExceptionError };
+
+    for (auto& applePayError : applePayErrors) {
+        if (applePayError)
+            errors.append({ applePayError->code(), applePayError->message(), applePayError->contactField() });
     }
 #else
-    UNUSED_PARAM(paymentMethodErrors);
+    UNUSED_PARAM(errors);
 #endif
 
-    return errors;
+    return { };
 }
 
 ExceptionOr<void> ApplePayPaymentHandler::detailsUpdated(PaymentRequest::UpdateReason reason, String&& error, AddressErrors&& addressErrors, PayerErrorFields&& payerErrors, JSC::JSObject* paymentMethodErrors)
@@ -458,6 +485,7 @@ ExceptionOr<void> ApplePayPaymentHandler::paymentMethodUpdated()
 void ApplePayPaymentHandler::complete(std::optional<PaymentComplete>&& result)
 {
     if (!result) {
+        ASSERT(isFinalStateResult(std::nullopt));
         paymentCoordinator().completePaymentSession(std::nullopt);
         return;
     }
@@ -473,9 +501,31 @@ void ApplePayPaymentHandler::complete(std::optional<PaymentComplete>&& result)
         break;
     }
 
+    ASSERT(isFinalStateResult(authorizationResult));
     paymentCoordinator().completePaymentSession(WTFMove(authorizationResult));
 }
 
+ExceptionOr<void> ApplePayPaymentHandler::retry(PaymentValidationErrors&& validationErrors)
+{
+    Vector<PaymentError> errors;
+
+    computeAddressErrors(WTFMove(validationErrors.error), WTFMove(validationErrors.shippingAddress), errors);
+    computePayerErrors(WTFMove(validationErrors.payer), errors);
+
+    auto exception = computePaymentMethodErrors(validationErrors.paymentMethod.get(), errors);
+    if (exception.hasException())
+        return exception.releaseException();
+
+    // Ensure there is always at least one error to avoid having a final result.
+    if (errors.isEmpty())
+        errors.append({ PaymentError::Code::Unknown, { }, std::nullopt });
+
+    PaymentAuthorizationResult authorizationResult { PaymentAuthorizationStatus::Failure, WTFMove(errors) };
+    ASSERT(!isFinalStateResult(authorizationResult));
+    paymentCoordinator().completePaymentSession(WTFMove(authorizationResult));
+    return { };
+}
+
 unsigned ApplePayPaymentHandler::version() const
 {
     if (!m_applePayRequest)
index ce5053a..5f42ac6 100644 (file)
@@ -53,8 +53,11 @@ private:
     PaymentCoordinator& paymentCoordinator() const;
 
     ExceptionOr<Vector<ApplePaySessionPaymentRequest::ShippingMethod>> computeShippingMethods();
-    ExceptionOr<ApplePaySessionPaymentRequest::TotalAndLineItems> computeTotalAndLineItems();
+    ExceptionOr<ApplePaySessionPaymentRequest::TotalAndLineItems> computeTotalAndLineItems() const;
     Vector<PaymentError> computeErrors(String&& error, AddressErrors&&, PayerErrorFields&&, JSC::JSObject* paymentMethodErrors) const;
+    void computeAddressErrors(String&& error, AddressErrors&&, Vector<PaymentError>&) const;
+    void computePayerErrors(PayerErrorFields&&, Vector<PaymentError>&) const;
+    ExceptionOr<void> computePaymentMethodErrors(JSC::JSObject* paymentMethodErrors, Vector<PaymentError>&) const;
 
     ExceptionOr<void> shippingAddressUpdated(Vector<PaymentError>&& errors);
     ExceptionOr<void> shippingOptionUpdated();
@@ -68,6 +71,7 @@ private:
     ExceptionOr<void> detailsUpdated(PaymentRequest::UpdateReason, String&& error, AddressErrors&&, PayerErrorFields&&, JSC::JSObject* paymentMethodErrors) final;
     ExceptionOr<void> merchantValidationCompleted(JSC::JSValue&&) final;
     void complete(std::optional<PaymentComplete>&&) final;
+    ExceptionOr<void> retry(PaymentValidationErrors&&) final;
 
     // PaymentSession
     unsigned version() const final;
index 1735872..a6f50ea 100644 (file)
@@ -40,6 +40,7 @@ namespace WebCore {
 class Document;
 struct AddressErrors;
 struct PayerErrorFields;
+struct PaymentValidationErrors;
 
 class PaymentHandler : public virtual PaymentSessionBase {
 public:
@@ -54,6 +55,7 @@ public:
     virtual ExceptionOr<void> detailsUpdated(PaymentRequest::UpdateReason, String&& error, AddressErrors&&, PayerErrorFields&&, JSC::JSObject* paymentMethodErrors) = 0;
     virtual ExceptionOr<void> merchantValidationCompleted(JSC::JSValue&&) = 0;
     virtual void complete(std::optional<PaymentComplete>&&) = 0;
+    virtual ExceptionOr<void> retry(PaymentValidationErrors&&) = 0;
 };
 
 } // namespace WebCore
index c9dbb86..2b2477f 100644 (file)
@@ -42,6 +42,7 @@
 #include "PaymentMethodData.h"
 #include "PaymentOptions.h"
 #include "PaymentRequestUpdateEvent.h"
+#include "PaymentValidationErrors.h"
 #include "ScriptController.h"
 #include <JavaScriptCore/JSONObject.h>
 #include <JavaScriptCore/ThrowScope.h>
@@ -404,7 +405,7 @@ void PaymentRequest::show(Document& document, RefPtr<DOMPromise>&& detailsPromis
     for (auto& paymentMethod : m_serializedMethodData) {
         auto data = parse(document, paymentMethod.serializedData);
         if (data.hasException()) {
-            m_showPromise->reject(data.releaseException());
+            settleShowPromise(data.releaseException());
             return;
         }
 
@@ -414,7 +415,7 @@ void PaymentRequest::show(Document& document, RefPtr<DOMPromise>&& detailsPromis
 
         auto result = handler->convertData(data.releaseReturnValue());
         if (result.hasException()) {
-            m_showPromise->reject(result.releaseException());
+            settleShowPromise(result.releaseException());
             return;
         }
 
@@ -423,13 +424,13 @@ void PaymentRequest::show(Document& document, RefPtr<DOMPromise>&& detailsPromis
     }
 
     if (!selectedPaymentHandler) {
-        m_showPromise->reject(Exception { NotSupportedError });
+        settleShowPromise(Exception { NotSupportedError });
         return;
     }
 
     auto exception = selectedPaymentHandler->show();
     if (exception.hasException()) {
-        m_showPromise->reject(exception.releaseException());
+        settleShowPromise(exception.releaseException());
         return;
     }
 
@@ -445,32 +446,51 @@ void PaymentRequest::show(Document& document, RefPtr<DOMPromise>&& detailsPromis
 
 void PaymentRequest::abortWithException(Exception&& exception)
 {
-    if (m_state != State::Interactive)
-        return;
+    ASSERT(m_state == State::Interactive);
+    closeActivePaymentHandler();
 
-    if (auto paymentHandler = activePaymentHandler())
-        paymentHandler->hide();
-    m_activePaymentHandler = std::nullopt;
+    if (m_response)
+        m_response->abortWithException(WTFMove(exception));
+    else
+        settleShowPromise(WTFMove(exception));
+}
 
-    ASSERT(m_state == State::Interactive);
+void PaymentRequest::settleShowPromise(ExceptionOr<PaymentResponse&>&& result)
+{
+    if (auto showPromise = std::exchange(m_showPromise, std::nullopt))
+        showPromise->settle(WTFMove(result));
+}
+
+void PaymentRequest::closeActivePaymentHandler()
+{
+    if (auto activePaymentHandler = std::exchange(m_activePaymentHandler, std::nullopt))
+        activePaymentHandler->paymentHandler->hide();
+
+    m_isUpdating = false;
     m_state = State::Closed;
-    m_showPromise->reject(WTFMove(exception));
 }
 
 void PaymentRequest::stop()
 {
-    abortWithException(Exception { AbortError });
+    closeActivePaymentHandler();
+    settleShowPromise(Exception { AbortError });
 }
 
 // https://www.w3.org/TR/payment-request/#abort()-method
-ExceptionOr<void> PaymentRequest::abort(AbortPromise&& promise)
+void PaymentRequest::abort(AbortPromise&& promise)
 {
-    if (m_state != State::Interactive)
-        return Exception { InvalidStateError };
+    if (m_response && m_response->hasRetryPromise()) {
+        promise.reject(Exception { InvalidStateError });
+        return;
+    }
+
+    if (m_state != State::Interactive) {
+        promise.reject(Exception { InvalidStateError });
+        return;
+    }
 
-    stop();
+    abortWithException(Exception { AbortError });
     promise.resolve();
-    return { };
 }
 
 // https://www.w3.org/TR/payment-request/#canmakepayment()-method
@@ -587,7 +607,7 @@ ExceptionOr<void> PaymentRequest::completeMerchantValidation(Event& event, Ref<D
             return;
 
         if (m_merchantSessionPromise->status() == DOMPromise::Status::Rejected) {
-            stop();
+            abortWithException(Exception { AbortError });
             return;
         }
 
@@ -613,7 +633,7 @@ void PaymentRequest::settleDetailsPromise(UpdateReason reason)
         return;
 
     if (m_isCancelPending || m_detailsPromise->status() == DOMPromise::Status::Rejected) {
-        stop();
+        abortWithException(Exception { AbortError });
         return;
     }
 
@@ -679,42 +699,55 @@ void PaymentRequest::whenDetailsSettled(std::function<void()>&& callback)
 
 void PaymentRequest::accept(const String& methodName, PaymentResponse::DetailsFunction&& detailsFunction, Ref<PaymentAddress>&& shippingAddress, const String& payerName, const String& payerEmail, const String& payerPhone)
 {
+    ASSERT(!m_isUpdating);
     ASSERT(m_state == State::Interactive);
 
-    auto response = PaymentResponse::create(scriptExecutionContext(), *this, WTFMove(detailsFunction));
-    response->setRequestId(m_details.id);
-    response->setMethodName(methodName);
-
-    if (m_options.requestShipping) {
-        response->setShippingAddress(shippingAddress.ptr());
-        response->setShippingOption(m_shippingOption);
+    bool isRetry = m_response;
+    if (!isRetry) {
+        m_response = PaymentResponse::create(scriptExecutionContext(), *this, WTFMove(detailsFunction));
+        m_response->setRequestId(m_details.id);
     }
 
-    if (m_options.requestPayerName)
-        response->setPayerName(payerName);
-
-    if (m_options.requestPayerEmail)
-        response->setPayerEmail(payerEmail);
-
-    if (m_options.requestPayerPhone)
-        response->setPayerPhone(payerPhone);
+    m_response->setMethodName(methodName);
+    m_response->setShippingAddress(m_options.requestShipping ? shippingAddress.ptr() : nullptr);
+    m_response->setShippingOption(m_options.requestShipping ? m_shippingOption : String { });
+    m_response->setPayerName(m_options.requestPayerName ? payerName : String { });
+    m_response->setPayerEmail(m_options.requestPayerEmail ? payerEmail : String { });
+    m_response->setPayerPhone(m_options.requestPayerPhone ? payerPhone : String { });
+
+    if (!isRetry)
+        settleShowPromise(*m_response);
+    else {
+        ASSERT(m_response->hasRetryPromise());
+        m_response->settleRetryPromise();
+    }
 
-    m_showPromise->resolve(response.get());
     m_state = State::Closed;
 }
 
-void PaymentRequest::complete(std::optional<PaymentComplete>&& result)
+ExceptionOr<void> PaymentRequest::complete(std::optional<PaymentComplete>&& result)
 {
     ASSERT(m_state == State::Closed);
+    if (!m_activePaymentHandler)
+        return Exception { AbortError };
+
     activePaymentHandler()->complete(WTFMove(result));
     m_activePaymentHandler = std::nullopt;
+    return { };
 }
 
-void PaymentRequest::cancel()
+ExceptionOr<void> PaymentRequest::retry(PaymentValidationErrors&& errors)
 {
-    if (m_state != State::Interactive)
-        return;
+    ASSERT(m_state == State::Closed);
+    if (!m_activePaymentHandler)
+        return Exception { AbortError };
+
+    m_state = State::Interactive;
+    return activePaymentHandler()->retry(WTFMove(errors));
+}
 
+void PaymentRequest::cancel()
+{
     m_activePaymentHandler = std::nullopt;
 
     if (m_isUpdating) {
@@ -722,7 +755,7 @@ void PaymentRequest::cancel()
         return;
     }
 
-    stop();
+    abortWithException(Exception { AbortError });
 }
 
 } // namespace WebCore
index acbcbf4..9e215e5 100644 (file)
@@ -50,7 +50,7 @@ enum class PaymentShippingType;
 struct PaymentDetailsUpdate;
 struct PaymentMethodData;
 
-class PaymentRequest final : public RefCounted<PaymentRequest>, public ActiveDOMObject, public EventTargetWithInlineData {
+class PaymentRequest final : public ActiveDOMObject, public CanMakeWeakPtr<PaymentRequest>, public EventTargetWithInlineData, public RefCounted<PaymentRequest> {
 public:
     using AbortPromise = DOMPromiseDeferred<void>;
     using CanMakePaymentPromise = DOMPromiseDeferred<IDLBoolean>;
@@ -60,7 +60,7 @@ public:
     ~PaymentRequest();
 
     void show(Document&, RefPtr<DOMPromise>&& detailsPromise, ShowPromise&&);
-    ExceptionOr<void> abort(AbortPromise&&);
+    void abort(AbortPromise&&);
     void canMakePayment(Document&, CanMakePaymentPromise&&);
 
     const String& id() const;
@@ -93,7 +93,8 @@ public:
     ExceptionOr<void> updateWith(UpdateReason, Ref<DOMPromise>&&);
     ExceptionOr<void> completeMerchantValidation(Event&, Ref<DOMPromise>&&);
     void accept(const String& methodName, PaymentResponse::DetailsFunction&&, Ref<PaymentAddress>&& shippingAddress, const String& payerName, const String& payerEmail, const String& payerPhone);
-    void complete(std::optional<PaymentComplete>&&);
+    ExceptionOr<void> complete(std::optional<PaymentComplete>&&);
+    ExceptionOr<void> retry(PaymentValidationErrors&&);
     void cancel();
 
     using MethodIdentifier = Variant<String, URL>;
@@ -117,6 +118,8 @@ private:
     void whenDetailsSettled(std::function<void()>&&);
     void abortWithException(Exception&&);
     PaymentHandler* activePaymentHandler() { return m_activePaymentHandler ? m_activePaymentHandler->paymentHandler.ptr() : nullptr; }
+    void settleShowPromise(ExceptionOr<PaymentResponse&>&&);
+    void closeActivePaymentHandler();
 
     // ActiveDOMObject
     const char* activeDOMObjectName() const final { return "PaymentRequest"; }
@@ -141,6 +144,7 @@ private:
     std::optional<PaymentHandlerWithPendingActivity> m_activePaymentHandler;
     RefPtr<DOMPromise> m_detailsPromise;
     RefPtr<DOMPromise> m_merchantSessionPromise;
+    RefPtr<PaymentResponse> m_response;
     bool m_isUpdating { false };
     bool m_isCancelPending { false };
 };
index 7fc3e7c..b9c1264 100644 (file)
@@ -33,7 +33,7 @@
     SecureContext,
 ] interface PaymentRequest : EventTarget {
     [CallWith=Document] Promise<PaymentResponse> show(optional Promise<PaymentDetailsUpdate> detailsPromise);
-    [MayThrowException] Promise<void> abort();
+    Promise<void> abort();
     [CallWith=Document] Promise<boolean> canMakePayment();
 
     readonly attribute DOMString id;
index cf0ac4f..5643fa4 100644 (file)
 namespace WebCore {
 
 PaymentResponse::PaymentResponse(ScriptExecutionContext* context, PaymentRequest& request, DetailsFunction&& detailsFunction)
-    : ContextDestructionObserver { context }
-    , m_request { request }
+    : ActiveDOMObject { context }
+    , m_request { makeWeakPtr(request) }
     , m_detailsFunction { WTFMove(detailsFunction) }
 {
     ASSERT(m_detailsFunction);
+    suspendIfNeeded();
 }
 
-PaymentResponse::~PaymentResponse() = default;
+void PaymentResponse::finishConstruction()
+{
+    ASSERT(!hasPendingActivity());
+    m_pendingActivity = makePendingActivity(*this);
+}
+
+PaymentResponse::~PaymentResponse()
+{
+    ASSERT(!hasPendingActivity());
+    ASSERT(!hasRetryPromise());
+}
 
 void PaymentResponse::complete(std::optional<PaymentComplete>&& result, DOMPromiseDeferred<void>&& promise)
 {
-    if (m_completeCalled) {
+    if (m_state == State::Stopped || !m_request) {
+        promise.reject(Exception { AbortError });
+        return;
+    }
+
+    if (m_state == State::Completed || m_retryPromise) {
+        promise.reject(Exception { InvalidStateError });
+        return;
+    }
+
+    ASSERT(hasPendingActivity());
+    ASSERT(m_state == State::Created);
+    m_pendingActivity = nullptr;
+    m_state = State::Completed;
+
+    promise.settle(m_request->complete(WTFMove(result)));
+}
+
+void PaymentResponse::retry(PaymentValidationErrors&& errors, DOMPromiseDeferred<void>&& promise)
+{
+    if (m_state == State::Stopped || !m_request) {
+        promise.reject(Exception { AbortError });
+        return;
+    }
+
+    if (m_state == State::Completed || m_retryPromise) {
         promise.reject(Exception { InvalidStateError });
         return;
     }
 
-    m_completeCalled = true;
-    m_request->complete(WTFMove(result));
-    promise.resolve();
+    ASSERT(hasPendingActivity());
+    ASSERT(m_state == State::Created);
+
+    auto exception = m_request->retry(WTFMove(errors));
+    if (exception.hasException()) {
+        promise.reject(exception.releaseException());
+        return;
+    }
+
+    m_retryPromise = WTFMove(promise);
+}
+
+void PaymentResponse::abortWithException(Exception&& exception)
+{
+    settleRetryPromise(WTFMove(exception));
+    m_pendingActivity = nullptr;
+    m_state = State::Completed;
+}
+
+void PaymentResponse::settleRetryPromise(ExceptionOr<void>&& result)
+{
+    if (!m_retryPromise)
+        return;
+
+    ASSERT(hasPendingActivity());
+    ASSERT(m_state == State::Created);
+    std::exchange(m_retryPromise, std::nullopt)->settle(WTFMove(result));
+}
+
+bool PaymentResponse::canSuspendForDocumentSuspension() const
+{
+    ASSERT(m_state != State::Stopped);
+    return !hasPendingActivity();
 }
 
-void PaymentResponse::retry(PaymentValidationErrors&&, DOMPromiseDeferred<void>&& promise)
+void PaymentResponse::stop()
 {
-    notImplemented();
-    promise.reject(Exception { NotSupportedError });
+    settleRetryPromise(Exception { AbortError });
+    m_pendingActivity = nullptr;
+    m_state = State::Stopped;
 }
 
 } // namespace WebCore
index c365dfa..f7f0f57 100644 (file)
 
 #if ENABLE(PAYMENT_REQUEST)
 
+#include "ActiveDOMObject.h"
 #include "ContextDestructionObserver.h"
 #include "EventTarget.h"
 #include "JSDOMPromiseDeferred.h"
 #include "JSValueInWrappedObject.h"
 #include "PaymentAddress.h"
 #include "PaymentComplete.h"
+#include <wtf/WeakPtr.h>
 
 namespace WebCore {
 
@@ -40,13 +42,15 @@ class Document;
 class PaymentRequest;
 struct PaymentValidationErrors;
 
-class PaymentResponse final : public ContextDestructionObserver, public EventTargetWithInlineData, public RefCounted<PaymentResponse> {
+class PaymentResponse final : public ActiveDOMObject, public EventTargetWithInlineData, public RefCounted<PaymentResponse> {
 public:
     using DetailsFunction = Function<JSC::Strong<JSC::JSObject>(JSC::ExecState&)>;
 
     static Ref<PaymentResponse> create(ScriptExecutionContext* context, PaymentRequest& request, DetailsFunction&& detailsFunction)
     {
-        return adoptRef(*new PaymentResponse(context, request, WTFMove(detailsFunction)));
+        auto response = adoptRef(*new PaymentResponse(context, request, WTFMove(detailsFunction)));
+        response->finishConstruction();
+        return response;
     }
 
     ~PaymentResponse();
@@ -77,20 +81,35 @@ public:
 
     void complete(std::optional<PaymentComplete>&&, DOMPromiseDeferred<void>&&);
     void retry(PaymentValidationErrors&&, DOMPromiseDeferred<void>&&);
+    void abortWithException(Exception&&);
+    bool hasRetryPromise() const { return !!m_retryPromise; }
+    void settleRetryPromise(ExceptionOr<void>&& = { });
 
     using RefCounted<PaymentResponse>::ref;
     using RefCounted<PaymentResponse>::deref;
 
 private:
     PaymentResponse(ScriptExecutionContext*, PaymentRequest&, DetailsFunction&&);
+    void finishConstruction();
+
+    // ActiveDOMObject
+    const char* activeDOMObjectName() const final { return "PaymentResponse"; }
+    bool canSuspendForDocumentSuspension() const final;
+    void stop() final;
 
     // EventTarget
     EventTargetInterface eventTargetInterface() const final { return PaymentResponseEventTargetInterfaceType; }
-    ScriptExecutionContext* scriptExecutionContext() const final { return ContextDestructionObserver::scriptExecutionContext(); }
+    ScriptExecutionContext* scriptExecutionContext() const final { return ActiveDOMObject::scriptExecutionContext(); }
     void refEventTarget() final { ref(); }
     void derefEventTarget() final { deref(); }
 
-    Ref<PaymentRequest> m_request;
+    enum class State {
+        Created,
+        Completed,
+        Stopped,
+    };
+
+    WeakPtr<PaymentRequest> m_request;
     String m_requestId;
     String m_methodName;
     DetailsFunction m_detailsFunction;
@@ -100,7 +119,9 @@ private:
     String m_payerName;
     String m_payerEmail;
     String m_payerPhone;
-    bool m_completeCalled { false };
+    State m_state { State::Created };
+    std::optional<DOMPromiseDeferred<void>> m_retryPromise;
+    RefPtr<PendingActivity<PaymentResponse>> m_pendingActivity;
 };
 
 } // namespace WebCore
index df96f8b..8efaf2a 100644 (file)
@@ -547,9 +547,9 @@ Internals::Internals(Document& document)
 
 #if ENABLE(APPLE_PAY)
     auto* frame = document.frame();
-    if (frame && frame->page()) {
-        m_mockPaymentCoordinator = new MockPaymentCoordinator(*frame->page());
-        frame->page()->setPaymentCoordinator(std::make_unique<PaymentCoordinator>(*m_mockPaymentCoordinator));
+    if (frame && frame->page() && frame->isMainFrame()) {
+        auto mockPaymentCoordinator = new MockPaymentCoordinator(*frame->page());
+        frame->page()->setPaymentCoordinator(std::make_unique<PaymentCoordinator>(*mockPaymentCoordinator));
     }
 #endif
 }
@@ -4625,9 +4625,9 @@ bool Internals::hasServiceWorkerConnection()
 #endif
 
 #if ENABLE(APPLE_PAY)
-MockPaymentCoordinator& Internals::mockPaymentCoordinator() const
+MockPaymentCoordinator& Internals::mockPaymentCoordinator(Document& document)
 {
-    return *m_mockPaymentCoordinator;
+    return downcast<MockPaymentCoordinator>(document.frame()->page()->paymentCoordinator().client());
 }
 #endif
 
index 62f9a46..01d4dd6 100644 (file)
@@ -698,7 +698,7 @@ public:
 #endif
 
 #if ENABLE(APPLE_PAY)
-    MockPaymentCoordinator& mockPaymentCoordinator() const;
+    MockPaymentCoordinator& mockPaymentCoordinator(Document&);
 #endif
 
     bool isSystemPreviewLink(Element&) const;
@@ -795,10 +795,6 @@ private:
 
     std::unique_ptr<InspectorStubFrontend> m_inspectorFrontend;
     RefPtr<CacheStorageConnection> m_cacheStorageConnection;
-
-#if ENABLE(APPLE_PAY)
-    MockPaymentCoordinator* m_mockPaymentCoordinator { nullptr };
-#endif
 };
 
 } // namespace WebCore
index 19dcf21..773042e 100644 (file)
@@ -682,7 +682,7 @@ enum CompositingPolicy {
     [Conditional=SERVICE_WORKER] void terminateServiceWorker(ServiceWorker worker);
     [Conditional=SERVICE_WORKER] boolean hasServiceWorkerConnection();
 
-    [Conditional=APPLE_PAY] readonly attribute MockPaymentCoordinator mockPaymentCoordinator;
+    [CallWith=Document, Conditional=APPLE_PAY] readonly attribute MockPaymentCoordinator mockPaymentCoordinator;
 
     boolean isSystemPreviewLink(Element element);
     boolean isSystemPreviewImage(Element element);
index 7d3b539..1a848d8 100644 (file)
@@ -28,6 +28,7 @@
 
 #if ENABLE(APPLE_PAY)
 
+#include "ApplePaySessionPaymentRequest.h"
 #include "MockPayment.h"
 #include "MockPaymentContact.h"
 #include "MockPaymentMethod.h"
@@ -209,7 +210,10 @@ void MockPaymentCoordinator::cancelPayment()
 
 void MockPaymentCoordinator::completePaymentSession(std::optional<PaymentAuthorizationResult>&& result)
 {
-    if (!isFinalStateResult(result))
+    auto isFinalState = isFinalStateResult(result);
+    m_errors = WTFMove(result->errors);
+
+    if (!isFinalState)
         return;
 
     ++hideCount;
index 8be4d35..9b5a3e5 100644 (file)
@@ -76,6 +76,8 @@ private:
     void cancelPaymentSession() final;
     void paymentCoordinatorDestroyed() final;
 
+    bool isMockPaymentCoordinator() const final { return true; }
+
     void updateTotalAndLineItems(const ApplePaySessionPaymentRequest::TotalAndLineItems&);
 
     Page& m_page;
@@ -91,4 +93,8 @@ private:
 
 } // namespace WebCore
 
+SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::MockPaymentCoordinator)
+    static bool isType(const WebCore::PaymentCoordinatorClient& paymentCoordinatorClient) { return paymentCoordinatorClient.isMockPaymentCoordinator(); }
+SPECIALIZE_TYPE_TRAITS_END()
+
 #endif // ENABLE(APPLE_PAY)