[Payments] Enable shipping and contact info delegation [4/5]

This cl implements shipping address/option change events for PH. With
this change payment handlers can notify the merchant when the user
changes the selected shipping address/option, and wait for updated
details (e.g. new shipping cost, etc) from merchant.

For overall flow please check
https://chromium-review.googlesource.com/c/chromium/src/+/1779003

Bug: 984694
Change-Id: Id881ba22bf4c846a4570801bacc49e5d4e89a72b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1804557
Reviewed-by: Mike West <mkwst@chromium.org>
Reviewed-by: Rouslan Solomakhin <rouslan@chromium.org>
Commit-Queue: Sahel Sharify <sahel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#700238}
diff --git a/payment-handler/app-change-shipping-address.js b/payment-handler/app-change-shipping-address.js
new file mode 100644
index 0000000..df39258
--- /dev/null
+++ b/payment-handler/app-change-shipping-address.js
@@ -0,0 +1,44 @@
+self.addEventListener('canmakepayment', (event) => {
+  event.respondWith(true);
+});
+
+async function responder(event) {
+  const methodName = event.methodData[0].supportedMethods;
+  const shippingOption = event.shippingOptions[0].id;
+  const shippingAddress = {
+    addressLine: [
+      '1875 Explorer St #1000',
+    ],
+    city: 'Reston',
+    country: 'US',
+    dependentLocality: '',
+    organization: 'Google',
+    phone: '+15555555555',
+    postalCode: '20190',
+    recipient: 'John Smith',
+    region: 'VA',
+    sortingCode: '',
+  };
+  if (!event.changeShippingAddress) {
+    return {
+      methodName,
+      details: {
+        changeShippingAddressReturned:
+          'The changeShippingAddress() method is not implemented.',
+      },
+    };
+  }
+  let changeShippingAddressReturned;
+  try {
+    const response = await event.changeShippingAddress(shippingAddress);
+    changeShippingAddressReturned = response;
+  } catch (err) {
+    changeShippingAddressReturned = err.message;
+  }
+  return {methodName, details: {changeShippingAddressReturned}, shippingAddress,
+      shippingOption};
+}
+
+self.addEventListener('paymentrequest', (event) => {
+  event.respondWith(responder(event));
+});
diff --git a/payment-handler/app-change-shipping-option.js b/payment-handler/app-change-shipping-option.js
new file mode 100644
index 0000000..ac3307b
--- /dev/null
+++ b/payment-handler/app-change-shipping-option.js
@@ -0,0 +1,44 @@
+self.addEventListener('canmakepayment', (event) => {
+  event.respondWith(true);
+});
+
+async function responder(event) {
+  const methodName = event.methodData[0].supportedMethods;
+  const shippingOption = event.shippingOptions[0].id;
+  const shippingAddress = {
+    addressLine: [
+      '1875 Explorer St #1000',
+    ],
+    city: 'Reston',
+    country: 'US',
+    dependentLocality: '',
+    organization: 'Google',
+    phone: '+15555555555',
+    postalCode: '20190',
+    recipient: 'John Smith',
+    region: 'VA',
+    sortingCode: '',
+  };
+  if (!event.changeShippingOption) {
+    return {
+      methodName,
+      details: {
+        changeShippingOptionReturned:
+          'The changeShippingOption() method is not implemented.',
+      },
+    };
+  }
+  let changeShippingOptionReturned;
+  try {
+    const response = await event.changeShippingOption(shippingOption);
+    changeShippingOptionReturned = response;
+  } catch (err) {
+    changeShippingOptionReturned = err.message;
+  }
+  return {methodName, details: {changeShippingOptionReturned}, shippingAddress,
+      shippingOption};
+}
+
+self.addEventListener('paymentrequest', (event) => {
+  event.respondWith(responder(event));
+});
diff --git a/payment-handler/change-shipping-address-manual.https.html b/payment-handler/change-shipping-address-manual.https.html
new file mode 100644
index 0000000..3b98d56
--- /dev/null
+++ b/payment-handler/change-shipping-address-manual.https.html
@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for PaymentRequestEvent.changeShippingAddress()</title>
+
+<link rel="manifest" href="/payment-handler/basic-card.json" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="register-and-activate-service-worker.js"></script>
+<p>If the payment sheet is shown, please authorize the mock payment.</p>
+<script>
+  const methodName = window.location.origin + '/payment-handler/payment-app/';
+  function createRequest() {
+    return new PaymentRequest([{supportedMethods: methodName}], {
+        total: {label: 'Total', amount: {currency: 'USD', value: '0.01'}},
+        shippingOptions: [{
+          id: 'freeShippingOption',
+          label: 'Free global shipping',
+          amount: {
+            currency: 'USD',
+            value: '0',
+          },
+          selected: false,
+        }],
+      }, {requestShipping: true});
+  }
+
+  async function completeAppSetUp(registration) {
+    await registration.paymentManager.instruments.clear();
+    await registration.paymentManager.instruments.set('instrument-key', {
+      name: 'Instrument Name',
+      method: methodName,
+    });
+    await navigator.serviceWorker.ready;
+    await registration.paymentManager.enableDelegations(['shippingAddress']);
+  }
+
+  async function runTests(registration) {
+    await completeAppSetUp(registration);
+    promise_test(async (t) => {
+      const request = createRequest();
+      // Intentionally do not respond to the 'shippingaddresschange' event.
+      const response = await test_driver.bless('showing a payment sheet', () =>
+        request.show()
+      );
+      const complete_promise = response.complete('success');
+
+      assert_equals(response.details.changeShippingAddressReturned, null);
+
+      return complete_promise;
+    }, 'If updateWith(details) is not run, changeShippingAddress() returns null.');
+
+    promise_test(async (t) => {
+      const request = createRequest();
+      request.addEventListener('shippingaddresschange', (event) => {
+        assert_equals(request.shippingAddress.organization, '', 'organization should be redacted');
+        assert_equals(request.shippingAddress.phone, '', 'phone should be redacted');
+        assert_equals(request.shippingAddress.recipient, '', 'recipient should be redacted');
+        assert_equals(request.shippingAddress.addressLine.length, 0, 'addressLine should be redacted');
+        assert_equals(request.shippingAddress.city, 'Reston');
+        assert_equals(request.shippingAddress.country, 'US');
+        assert_equals(request.shippingAddress.postalCode, '20190');
+        assert_equals(request.shippingAddress.region, 'VA');
+        event.updateWith({
+          total: {label: 'Total', amount: {currency: 'GBP', value: '0.02'}},
+          error: 'Error for test',
+          modifiers: [
+            {
+              supportedMethods: methodName,
+              data: {soup: 'potato'},
+              total: {
+                label: 'Modified total',
+                amount: {currency: 'EUR', value: '0.03'},
+              },
+              additionalDisplayItems: [
+                {
+                  label: 'Modified display item',
+                  amount: {currency: 'INR', value: '0.06'},
+                },
+              ],
+            },
+            {
+              supportedMethods: methodName + '2',
+              data: {soup: 'tomato'},
+              total: {
+                label: 'Modified total #2',
+                amount: {currency: 'CHF', value: '0.07'},
+              },
+              additionalDisplayItems: [
+                {
+                  label: 'Modified display item #2',
+                  amount: {currency: 'CAD', value: '0.08'},
+                },
+              ],
+            },
+          ],
+          displayItems: [
+            {
+              label: 'Display item',
+              amount: {currency: 'CNY', value: '0.04'},
+            },
+          ],
+          shippingOptions: [
+            {
+              id: 'freeShippingOption',
+              label: 'express global shipping',
+              amount: {
+                currency: 'USD',
+                value: '0',
+              },
+              selected: true,
+            }
+          ],
+           shippingAddressErrors: {
+            country: 'US only shipping',
+          }
+        });
+      });
+      const response = await test_driver.bless('showing a payment sheet', () =>
+        request.show()
+      );
+      const complete_promise = response.complete('success');
+      const changeShippingAddressReturned =
+        response.details.changeShippingAddressReturned;
+
+      assert_equals(changeShippingAddressReturned.total.currency, 'GBP');
+      assert_equals(changeShippingAddressReturned.total.value, '0.02');
+      assert_equals(changeShippingAddressReturned.total.label, undefined);
+      assert_equals(changeShippingAddressReturned.error, 'Error for test');
+      assert_equals(changeShippingAddressReturned.modifiers.length, 1);
+      assert_equals(changeShippingAddressReturned.displayItems, undefined);
+      assert_equals(changeShippingAddressReturned.shippingOptions.length, 1);
+      assert_equals(changeShippingAddressReturned.paymentMethodErrors, undefined);
+      assert_equals(changeShippingAddressReturned.shippingAddressErrors.country, 'US only shipping');
+
+      const shipping_option = changeShippingAddressReturned.shippingOptions[0];
+      assert_equals(shipping_option.id, 'freeShippingOption' );
+      assert_equals(shipping_option.label, 'express global shipping');
+      assert_equals(shipping_option.amount.currency, 'USD');
+      assert_equals(shipping_option.amount.value, '0');
+      assert_true(shipping_option.selected);
+
+      const modifier = changeShippingAddressReturned.modifiers[0];
+      assert_equals(modifier.supportedMethods, methodName);
+      assert_equals(modifier.data.soup, 'potato');
+      assert_equals(modifier.total.label, '');
+      assert_equals(modifier.total.amount.currency, 'EUR');
+      assert_equals(modifier.total.amount.value, '0.03');
+      assert_equals(modifier.additionalDisplayItems, undefined);
+
+      return complete_promise;
+    }, 'The changeShippingAddress() returns some details from the "shippingaddresschange" event\'s updateWith(details) call.');
+  }
+
+  registerAndActiveServiceWorker(
+    'app-change-shipping-address.js',
+    'payment-app/',
+    runTests
+  );
+</script>
diff --git a/payment-handler/change-shipping-option-manual.https.html b/payment-handler/change-shipping-option-manual.https.html
new file mode 100644
index 0000000..00d1aee
--- /dev/null
+++ b/payment-handler/change-shipping-option-manual.https.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for PaymentRequestEvent.changeShippingOption()</title>
+
+<link rel="manifest" href="/payment-handler/basic-card.json" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="register-and-activate-service-worker.js"></script>
+<p>If the payment sheet is shown, please authorize the mock payment.</p>
+<script>
+  const methodName = window.location.origin + '/payment-handler/payment-app/';
+  function createRequest() {
+    return new PaymentRequest([{supportedMethods: methodName}], {
+        total: {label: 'Total', amount: {currency: 'USD', value: '0.01'}},
+        shippingOptions: [{
+          id: 'freeShippingOption',
+          label: 'Free global shipping',
+          amount: {
+            currency: 'USD',
+            value: '0',
+          },
+          selected: false,
+        },
+        {
+          id: 'expressShippingOption',
+          label: 'express global shipping',
+          amount: {
+            currency: 'USD',
+            value: '0',
+          },
+          selected: true,
+        }],
+      }, {requestShipping: true});
+  }
+
+  async function completeAppSetUp(registration) {
+    await registration.paymentManager.instruments.clear();
+    await registration.paymentManager.instruments.set('instrument-key', {
+      name: 'Instrument Name',
+      method: methodName,
+    });
+    await navigator.serviceWorker.ready;
+    await registration.paymentManager.enableDelegations(['shippingAddress']);
+  }
+
+  async function runTests(registration) {
+    await completeAppSetUp(registration);
+    promise_test(async (t) => {
+      const request = createRequest();
+      // Intentionally do not respond to the 'shippingoptionchange' event.
+      const response = await test_driver.bless('showing a payment sheet', () =>
+        request.show()
+      );
+      const complete_promise = response.complete('success');
+
+      assert_equals(response.details.changeShippingOptionReturned, null);
+
+      return complete_promise;
+    }, 'If updateWith(details) is not run, changeShippingOption() returns null.');
+
+    promise_test(async (t) => {
+      const request = createRequest();
+      request.addEventListener('shippingoptionchange', (event) => {
+        assert_equals(request.shippingOption, 'freeShippingOption');
+        event.updateWith({
+          total: {label: 'Total', amount: {currency: 'GBP', value: '0.02'}},
+          error: 'Error for test',
+          modifiers: [
+            {
+              supportedMethods: methodName,
+              data: {soup: 'potato'},
+              total: {
+                label: 'Modified total',
+                amount: {currency: 'EUR', value: '0.03'},
+              },
+              additionalDisplayItems: [
+                {
+                  label: 'Modified display item',
+                  amount: {currency: 'INR', value: '0.06'},
+                },
+              ],
+            },
+            {
+              supportedMethods: methodName + '2',
+              data: {soup: 'tomato'},
+              total: {
+                label: 'Modified total #2',
+                amount: {currency: 'CHF', value: '0.07'},
+              },
+              additionalDisplayItems: [
+                {
+                  label: 'Modified display item #2',
+                  amount: {currency: 'CAD', value: '0.08'},
+                },
+              ],
+            },
+          ],
+          displayItems: [
+            {
+              label: 'Display item',
+              amount: {currency: 'CNY', value: '0.04'},
+            },
+          ],
+          shippingOptions: [
+            {
+              id: 'freeShippingOption',
+              label: 'express global shipping',
+              amount: {
+                currency: 'USD',
+                value: '0',
+              },
+              selected: true,
+            }
+          ],
+        });
+      });
+      const response = await test_driver.bless('showing a payment sheet', () =>
+        request.show()
+      );
+      const complete_promise = response.complete('success');
+      const changeShippingOptionReturned =
+        response.details.changeShippingOptionReturned;
+
+      assert_equals(changeShippingOptionReturned.total.currency, 'GBP');
+      assert_equals(changeShippingOptionReturned.total.value, '0.02');
+      assert_equals(changeShippingOptionReturned.total.label, undefined);
+      assert_equals(changeShippingOptionReturned.error, 'Error for test');
+      assert_equals(changeShippingOptionReturned.modifiers.length, 1);
+      assert_equals(changeShippingOptionReturned.displayItems, undefined);
+      assert_equals(changeShippingOptionReturned.shippingOptions.length, 1);
+      assert_equals(changeShippingOptionReturned.paymentMethodErrors, undefined);
+      assert_equals(changeShippingOptionReturned.shippingAddressErrors, undefined);
+
+      const shipping_option = changeShippingOptionReturned.shippingOptions[0];
+      assert_equals(shipping_option.id, 'freeShippingOption' );
+      assert_equals(shipping_option.label, 'express global shipping');
+      assert_equals(shipping_option.amount.currency, 'USD');
+      assert_equals(shipping_option.amount.value, '0');
+      assert_true(shipping_option.selected);
+
+      const modifier = changeShippingOptionReturned.modifiers[0];
+      assert_equals(modifier.supportedMethods, methodName);
+      assert_equals(modifier.data.soup, 'potato');
+      assert_equals(modifier.total.label, '');
+      assert_equals(modifier.total.amount.currency, 'EUR');
+      assert_equals(modifier.total.amount.value, '0.03');
+      assert_equals(modifier.additionalDisplayItems, undefined);
+
+      return complete_promise;
+    }, 'The changeShippingOption() returns some details from the "shippingoptionchange" event\'s updateWith(details) call.');
+  }
+
+  registerAndActiveServiceWorker(
+    'app-change-shipping-option.js',
+    'payment-app/',
+    runTests
+  );
+</script>