From 0a5afe29c87a8b303148839d5a474dbef5e33697 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcos=20C=C3=A1ceres?=
@@ -341,20 +346,44 @@
+ Here we see an example of how to add two shipping options to the
+ |details|.
+
+ Some financial transactions require a user to provide specific
+ information in order for a merchant to fulfill a purchase (e.g., the
+ user's shipping address, in case a physical good needs to be
+ shipped). To request this information, a merchant can pass a third
+ optional argument (|options:PaymentOptions |) to the
+ {{PaymentRequest}} constructor indicating what information they
+ require. When the payment request is shown, the user agent will
+ request this information from the end user and return it to the
+ merchant when the user accepts the payment request.
+
+ Prior to the user accepting to make payment, the site is given an
+ opportunity to update the payment request in response to user input.
+ This can include, for example, providing additional shipping options
+ (or modifying their cost), removing items that cannot ship to a
+ particular address, etc.
+
+ A developer can use the
+ {{PaymentDetailsUpdate/shippingAddressErrors}} member of the
+ {{PaymentDetailsUpdate}} dictionary to indicate that there are
+ validation errors with specific attributes of a {{ContactAddress}}.
+ The {{PaymentDetailsUpdate/shippingAddressErrors}} member is a
+ {{AddressErrors}} dictionary, whose members specifically demarcate
+ the fields of a [=physical address=] that are erroneous while also
+ providing helpful error messages to be displayed to the end user.
+
Payment Request API
@@ -274,10 +274,15 @@
label: "Value-Added Tax (VAT)",
amount: { currency: "GBP", value: "5.00" },
},
- {
- label: "Standard shipping",
- amount: { currency: "GBP", value: "5.00" },
- },
],
total: {
label: "Total due",
// The total is GBP£65.00 here because we need to
- // add tax and shipping.
+ // add shipping (below). The selected shipping
+ // costs GBP£5.00.
amount: { currency: "GBP", value: "65.00" },
},
};
+
+ Adding shipping options
+
+
+ const shippingOptions = [
+ {
+ id: "standard",
+ // Shipping by truck, 2 days
+ label: "🚛 Envío por camión (2 dias)",
+ amount: { currency: "EUR", value: "5.00" },
+ selected: true,
+ },
+ {
+ id: "drone",
+ // Drone shipping, 2 hours
+ label: "🚀 Drone Express (2 horas)",
+ amount: { currency: "EUR", value: "25.00" }
+ },
+ ];
+ Object.assign(details, { shippingOptions });
+
+
Conditional modifications to payment request
@@ -389,6 +418,30 @@
Object.assign(details, { modifiers });
+ Requesting specific information from the end user
+
+
+ const options = {
+ requestPayerEmail: false,
+ requestPayerName: true,
+ requestPayerPhone: false,
+ requestShipping: true,
+ }
+
+
Constructing a
PaymentRequest
@@ -401,7 +454,10 @@
async function doPaymentRequest() {
try {
- const request = new PaymentRequest(methodData, details);
+ const request = new PaymentRequest(methodData, details, options);
+ // See below for a detailed example of handling these events
+ request.onshippingaddresschange = ev => ev.updateWith(details);
+ request.onshippingoptionchange = ev => ev.updateWith(details);
const response = await request.show();
await validateResponse(response);
} catch (err) {
@@ -427,6 +483,80 @@
doPaymentRequest();
+ Handling events and updating the payment request
+
+
+ const request = new PaymentRequest(methodData, details, options);
+ // Async update to details
+ request.onshippingaddresschange = ev => {
+ ev.updateWith(checkShipping(request));
+ };
+ // Sync update to the total
+ request.onshippingoptionchange = ev => {
+ // selected shipping option
+ const { shippingOption } = request;
+ const newTotal = {
+ currency: "USD",
+ label: "Total due",
+ value: calculateNewTotal(shippingOption),
+ };
+ ev.updateWith({ total: newTotal });
+ };
+ async function checkShipping(request) {
+ try {
+ const { shippingAddress } = request;
+
+ await ensureCanShipTo(shippingAddress);
+ const { shippingOptions, total } = await calculateShipping(shippingAddress);
+
+ return { shippingOptions, total };
+ } catch (err) {
+ // Shows error to user in the payment sheet.
+ return { error: `Sorry! we can't ship to your address.` };
+ }
+ }
+
+
+ Fine-grained error reporting
+
+
+ request.onshippingaddresschange = ev => {
+ ev.updateWith(validateAddress(request.shippingAddress));
+ };
+ function validateAddress(shippingAddress) {
+ const error = "Can't ship to this address.";
+ const shippingAddressErrors = {
+ city: "FarmVille is not a real place.",
+ postalCode: "Unknown postal code for your country.",
+ };
+ // Empty shippingOptions implies that we can't ship
+ // to this address.
+ const shippingOptions = [];
+ return { error, shippingAddressErrors, shippingOptions };
+ }
+
+
POSTing payment response back to a server
@@ -493,7 +623,8 @@
interface PaymentRequest : EventTarget {
constructor(
sequence<PaymentMethodData> methodData,
- PaymentDetailsInit details
+ PaymentDetailsInit details,
+ optional PaymentOptions options = {}
);
[NewObject]
Promise<PaymentResponse> show(optional Promise<PaymentDetailsUpdate> detailsPromise);
@@ -503,7 +634,12 @@
Promise<boolean> canMakePayment();
readonly attribute DOMString id;
+ readonly attribute ContactAddress? shippingAddress;
+ readonly attribute DOMString? shippingOption;
+ readonly attribute PaymentShippingType? shippingType;
+ attribute EventHandler onshippingaddresschange;
+ attribute EventHandler onshippingoptionchange;
attribute EventHandler onpaymentmethodchange;
};
@@ -518,6 +654,12 @@
while the user is providing input (up to the point of user approval
or denial of the payment request).
+ The {{PaymentRequest/shippingAddress}}, + {{PaymentRequest/shippingOption}}, and + {{PaymentRequest/shippingType}} attributes are populated during + processing if the {{PaymentOptions/requestShipping}} member is set. +
A |request|'s payment-relevant browsing context is that @@ -541,14 +683,15 @@
The {{PaymentRequest}} is constructed using the supplied sequence of PaymentMethodData |methodData| including any payment - method specific {{PaymentMethodData/data}}, and the - PaymentDetailsInit |details|. + method specific {{PaymentMethodData/data}}, the + PaymentDetailsInit |details|, and the {{PaymentOptions}} + |options|.
The PaymentRequest(|methodData|,
- |details|)
constructor MUST act as follows:
+ |details|, |options|) constructor MUST act as follows:
sequence
<{{PaymentShippingOption}}>.
+ + A {{PaymentRequest}}'s {{PaymentRequest/shippingAddress}} attribute + is populated when the user provides a shipping address. It is null by + default. When a user provides a shipping address, the shipping + address changed algorithm runs. +
++ A {{PaymentRequest}}'s {{PaymentRequest/shippingType}} attribute is + the type of shipping used to fulfill the transaction. Its value is + either a {{PaymentShippingType}} enum value, or null if none is + provided by the developer during + [=PaymentRequest.PaymentRequest()|construction=] (see + {{PaymentOptions}}'s {{PaymentOptions/shippingType}} member). +
++ A {{PaymentRequest}}'s {{PaymentRequest/onshippingaddresschange}} + attribute is an {{EventHandler}} for a {{PaymentRequestUpdateEvent}} + named shippingaddresschange. +
++ A {{PaymentRequest}}'s {{PaymentRequest/shippingOption}} attribute is + populated when the user chooses a shipping option. It is null by + default. When a user chooses a shipping option, the shipping + option changed algorithm runs. +
++ A {{PaymentRequest}}'s {{PaymentRequest/onshippingoptionchange}} + attribute is an {{EventHandler}} for a {{PaymentRequestUpdateEvent}} + named shippingoptionchange. +
+@@ -1572,6 +1837,7 @@
dictionary PaymentDetailsBase { sequence<PaymentItem> displayItems; + sequence<PaymentShippingOption> shippingOptions; sequence<PaymentDetailsModifier> modifiers; };@@ -1588,6 +1854,41 @@
+ A sequence containing the different shipping options for the user + to choose from. +
++ If an item in the sequence has the + {{PaymentShippingOption/selected}} member set to true, then this + is the shipping option that will be used by default and + {{PaymentRequest/shippingOption}} will be set to the + {{PaymentShippingOption/id}} of this option without running the + shipping option changed algorithm. If more than one item + in the sequence has {{PaymentShippingOption/selected}} set to + true, then the user agent selects the last one in the + sequence. +
++ The {{PaymentDetailsBase/shippingOptions}} member is only used if + the {{PaymentRequest}} was constructed with {{PaymentOptions}} + and {{PaymentOptions/requestShipping}} set to true. +
+ +dictionary PaymentDetailsUpdate : PaymentDetailsBase { + DOMString error; PaymentItem total; + AddressErrors shippingAddressErrors; + PayerErrors payerErrors; object paymentMethodErrors; };@@ -1665,6 +1969,21 @@
+ enum PaymentShippingType { + "shipping", + "delivery", + "pickup" + }; ++
+ dictionary PaymentOptions { + boolean requestPayerName = false; + boolean requestBillingAddress = false; + boolean requestPayerEmail = false; + boolean requestPayerPhone = false; + boolean requestShipping = false; + PaymentShippingType shippingType = "shipping"; + }; ++
+ The {{PaymentOptions}} dictionary is passed to the {{PaymentRequest}} + constructor and provides information about the options desired for the + payment request. +
++ The {{PaymentOptions/shippingType}} member only affects the user + interface for the payment request. +
++ dictionary PaymentShippingOption { + required DOMString id; + required DOMString label; + required PaymentCurrencyAmount amount; + boolean selected = false; + }; ++
+ The {{PaymentShippingOption}} dictionary has members describing a + shipping option. Developers can provide the user with one or more + shipping options by calling the + {{PaymentRequestUpdateEvent/updateWith()}} method in response to a + change event. +
+@@ -1929,7 +2447,7 @@
+
The retry(|errorFields:PaymentValidationErrors|)
method
MUST act as follows:
dictionary PaymentValidationErrors { + PayerErrors payer; + AddressErrors shippingAddress; DOMString error; object paymentMethod; };
- The payment method identifier for the payment method - that the user selected to fulfill the transaction. -
-
- An {{object}} or dictionary generated by a payment
- method that a merchant can use to process or validate a
+
+ The {{PayerErrors}} is used to represent validation errors with one
+ or more payer details.
+
+ Payer details are any of the payer's name, payer's phone
+ number, and payer's email.
+
+ PayerErrors dictionary
+
+
+ dictionary PayerErrors {
+ DOMString email;
+ DOMString name;
+ DOMString phone;
+ };
+
+
+
+
+ const payer = {
+ email: "The domain is invalid.",
+ phone: "Unknown country code.",
+ name: "Not in database.",
+ };
+ await response.retry({ payer });
+
+
+ The payment method identifier for the payment method + that the user selected to fulfill the transaction. +
++ An {{object}} or dictionary generated by a payment + method that a merchant can use to process or validate a transaction (depending on the payment method).
+ If the {{PaymentOptions/requestShipping}} member was set to true in + the {{PaymentOptions}} passed to the {{PaymentRequest}} constructor, + then {{PaymentRequest/shippingAddress}} will be the full and final + [=shipping address=] chosen by the user. +
++ If the {{PaymentOptions/requestShipping}} member was set to true in + the {{PaymentOptions}} passed to the {{PaymentRequest}} constructor, + then {{PaymentRequest/shippingOption}} will be the + {{PaymentShippingOption/id}} attribute of the selected shipping + option. +
++ If the {{PaymentOptions/requestPayerName}} member was set to true in + the {{PaymentOptions}} passed to the {{PaymentRequest}} constructor, + then {{PaymentResponse/payerName}} will be the name provided by the + user. +
++ If the {{PaymentOptions/requestPayerEmail}} member was set to true in + the {{PaymentOptions}} passed to the {{PaymentRequest}} constructor, + then {{PaymentResponse/payerEmail}} will be the email address chosen + by the user. +
++ If the {{PaymentOptions/requestPayerPhone}} member was set to true in + the {{PaymentOptions}} passed to the {{PaymentRequest}} constructor, + then {{PaymentResponse/payerPhone}} will be the phone number chosen + by the user. +
++ Allows a developer to handle "payerdetailchange" events. +
++ The {{PaymentRequest}} interface allows a merchant to request from the + user [=physical address|physical addresses=] for the purposes of + shipping and/or billing. A shipping address and billing + address are [=physical address|physical addresses=]. +
++ dictionary AddressErrors { + DOMString addressLine; + DOMString city; + DOMString country; + DOMString dependentLocality; + DOMString organization; + DOMString phone; + DOMString postalCode; + DOMString recipient; + DOMString region; + DOMString sortingCode; + }; ++
+ The members of the {{AddressErrors}} dictionary represent validation + errors with specific parts of a [=physical address=]. Each dictionary + member has a dual function: firstly, its presence denotes that a + particular part of an address is suffering from a validation error. + Secondly, the string value allows the developer to describe the + validation error (and possibly how the end user can fix the error). +
++ Developers need to be aware that users might not have the ability to + fix certain parts of an address. As such, they need to be mindful not + to ask the user to fix things they might not have control over. +
+shippingaddresschange
+ shippingoptionchange
+ payerdetailchange
+ paymentmethodchange
@@ -2457,13 +3322,50 @@ + // ❌ Bad - this won't work! + request.onshippingaddresschange = async ev => { + // await goes to next tick, and updateWith() + // was not called. + const details = await getNewDetails(oldDetails); + // 💥 So it's now too late! updateWith() + // throws "InvalidStateError". + ev.updateWith(details); + }; + + // ✅ Good - UI will wait. + request.onshippingaddresschange = ev => { + // Calling updateWith() with a promise is ok 👍 + const promiseForNewDetails = getNewDetails(oldDetails); + ev.updateWith(promiseForNewDetails); + }; +
Additionally, {{PaymentRequestUpdateEvent/[[waitForUpdate]]}} prevents reuse of {{PaymentRequestUpdateEvent}}.
++ // ❌ Bad - calling updateWith() twice doesn't work! + request.addEventListener("shippingaddresschange", ev => { + ev.updateWith(details); // this is ok. + // 💥 [[waitForUpdate]] is true, throws "InvalidStateError". + ev.updateWith(otherDetails); + }); + + // ❌ Bad - this won't work either! + request.addEventListener("shippingaddresschange", async ev => { + const p = Promise.resolve({ ...details }); + ev.updateWith(p); + await p; + // 💥 Only one call to updateWith() is allowed, + // so the following throws "InvalidStateError" + ev.updateWith({ ...newDetails }); + }); +
+ "PaymentRequestUpdateEvent/updatewith-method.https.html, PaymentRequestUpdateEvent/updateWith-incremental-update-manual.https.html"> The {{PaymentRequestUpdateEvent/updateWith()}} with |detailsPromise:Promise| method MUST act as follows:
@@ -2612,6 +3514,88 @@+ The shipping address changed algorithm runs when the user + provides a new shipping address. It MUST run the following steps: +
++ The |redactList| limits the amount of personal information + about the recipient that the API shares with the merchant. +
++ For merchants, the resulting {{ContactAddress}} object + provides enough information to, for example, calculate + shipping costs, but, in most cases, not enough information + to physically locate and uniquely identify the recipient. +
++ Unfortunately, even with the |redactList|, recipient + anonymity cannot be assured. This is because in some + countries postal codes are so fine-grained that they can + uniquely identify a recipient. +
++ The shipping option changed algorithm runs when the user + chooses a new shipping option. It MUST run the following steps: +
+id
string of the
+ {{PaymentShippingOption}} provided by the user.
+ + When the user selects or changes a payment method (e.g., a credit + card), the {{PaymentMethodChangeEvent}} includes redacted billing + address information for the purpose of performing tax calculations. + Redacted attributes include, but are not limited to, [=physical + address/address line=], [=physical address/dependent locality=], + [=physical address/organization=], [=physical address/phone number=], + and [=physical address/recipient=]. +
+
The PaymentRequest updated algorithm is run by other algorithms above to fire an event to indicate that a user has made a change to a {{PaymentRequest}} called |request| with an event @@ -2688,9 +3682,83 @@
+ The user agent MUST run the payer detail changed algorithm + when the user changes the |payer name|, or the |payer email|, or the + |payer phone| in the user interface: +
+The user accepts the payment request algorithm runs when the user accepts the payment request and confirms that they want @@ -2710,6 +3778,13 @@
sequence
<{{PaymentShippingOption}}>.
+ + If + |request|.{{PaymentRequest/[[options]]}}.{{PaymentOptions/requestShipping}} + is true, and + |request|.{{PaymentRequest/[[details]]}}.{{PaymentDetailsBase/shippingOptions}} + is empty, then the developer has signified that there are + no valid shipping options for the currently-chosen + shipping address (given by |request|'s + {{PaymentRequest/shippingAddress}}). +
++ In this case, the user agent SHOULD display an error + indicating this, and MAY indicate that the + currently-chosen shipping address is invalid in some way. + The user agent SHOULD use the + {{PaymentDetailsUpdate/error}} member of |details|, if it + is present, to give more information about why there are + no valid shipping options for that address. +
++ Further, if + |details|["{{PaymentDetailsUpdate/shippingAddressErrors}}"] + member is present, the user agent SHOULD display an error + specifically for each erroneous field of the shipping + address. This is done by matching each present member of + the {{AddressErrors}} to a corresponding input field in + the shown user interface. +
++ Similarly, if |details|["{{payerErrors}}"] member is + present and |request|.{{PaymentRequest/[[options]]}}'s + {{PaymentOptions/requestPayerName}}, + {{PaymentOptions/requestPayerEmail}}, or + {{PaymentOptions/requestPayerPhone}} is true, then + display an error specifically for each erroneous field. +
++ Likewise, if + |details|.{{PaymentDetailsUpdate/paymentMethodErrors}} is + present, then display errors specifically for each + erroneous input field for the particular payment method. +
+The user agent MUST NOT share information about the user with - a developer without user consent. + a developer (e.g., the [=shipping address=]) without user consent.
In particular, the {{PaymentMethodData}}'s {{PaymentMethodData/data}} @@ -3213,8 +4431,23 @@
Where sharing of privacy-sensitive information might not be obvious to users (e.g., when [=payment handler/payment method changed @@ -3267,7 +4500,9 @@
For the user-facing aspects of Payment Request API, implementations integrate with platform accessibility APIs via form controls and other - input modalities. + input modalities. Furthermore, to increase the intelligibility of + total, shipping addresses, and contact information, implementations + format data according to system conventions.