diff --git a/index.html b/index.html index 37189695..f03d5cc4 100644 --- a/index.html +++ b/index.html @@ -292,10 +292,15 @@
@@ -359,20 +364,44 @@
+ Here we see an example of how to add two shipping options to the + |details|. +
++ 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 }); ++
+ 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|) 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. +
++ const options = { + requestPayerEmail: false, + requestPayerName: true, + requestPayerPhone: false, + requestShipping: true, + } ++
PaymentRequest
@@ -420,6 +473,9 @@ + 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. +
++ 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 json = request.shippingAddress.toJSON(); + + await ensureCanShipTo(json); + const { shippingOptions, total } = await calculateShipping(json); + + return { shippingOptions, total }; + } catch (err) { + return { error: `Sorry! we can't ship to your address.` }; + } + } ++
+ A developer can use the + {{PaymentDetailsUpdate/shippingAddressErrors}} member of the + {{PaymentDetailsUpdate}} dictionary to indicate that there are + validation errors with specific attributes of a {{PaymentAddress}}. + 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. +
++ 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 }; + } ++
+ 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 @@ -559,14 +700,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. +
++
A {{PaymentRequest}}'s {{PaymentRequest/onpaymentmethodchange}} attribute is an {{EventHandler}} for a {{PaymentMethodChangeEvent}} named "paymentmethodchange". @@ -1301,7 +1556,16 @@
@@ -1596,6 +1860,7 @@
dictionary PaymentDetailsBase { sequence<PaymentItem> displayItems; + sequence<PaymentShippingOption> shippingOptions; sequence<PaymentDetailsModifier> modifiers; };@@ -1612,6 +1877,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; };@@ -1689,6 +1992,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. +
+@@ -1953,7 +2468,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. -
-
+
+ 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). @@ -2136,6 +2752,66 @@
+ 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. +
++ Instances of {{PaymentResponse}} are created with the [=internal + slots=] in the following table: +
++ Internal Slot + | ++ Description (non-normative) + | +
---|---|
+ [[\complete]] + | ++ Is true if the request for payment has completed (i.e., + {{PaymentResponse/complete()}} was called, or there was a fatal + error that made the response not longer usable), or false + otherwise. + | +
+ [[\request]] + | ++ The {{PaymentRequest}} instance that instantiated this + {{PaymentResponse}}. + | +
+ [[\retryPromise]] + | ++ Null, or a {{Promise}} that resolves when a user accepts the + payment request or rejects if the user aborts the payment + request. + | +
+ A physical address is composed of the following parts. +
++ [SecureContext, Exposed=(Window)] + interface PaymentAddress { + [Default] object toJSON(); + readonly attribute DOMString city; + readonly attribute DOMString country; + readonly attribute DOMString dependentLocality; + readonly attribute DOMString organization; + readonly attribute DOMString phone; + readonly attribute DOMString postalCode; + readonly attribute DOMString recipient; + readonly attribute DOMString region; + readonly attribute DOMString sortingCode; + readonly attribute FrozenArray<DOMString> addressLine; + }; ++
+ The {{PaymentAddress}} interface represents a physical + address. +
+ ++ The steps to internally construct a + `PaymentAddress` with an optional {{AddressInit}} + |details:AddressInit| are given by the following algorithm: +
++ Represents the country of the address. When getting, returns + the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[country]]}} internal slot. +
++ Represents the address line of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[addressLine]]}} internal slot. +
++ Represents the region of the address. When getting, returns + the value of the {{PaymentAddress}}'s {{PaymentAddress/[[region]]}} + internal slot. +
++ Represents the city of the address. When getting, returns + the value of the {{PaymentAddress}}'s {{PaymentAddress/[[city]]}} + internal slot. +
++ Represents the dependent locality of the address. When + getting, returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[dependentLocality]]}} internal slot. +
++ Represents the postal code of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[postalCode]]}} internal slot. +
++ Represents the sorting code of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[sortingCode]]}} internal slot. +
++ Represents the organization of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[organization]]}} internal slot. +
++ Represents the recipient of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[recipient]]}} internal slot. +
++ Represents the phone number of the address. When getting, + returns the value of the {{PaymentAddress}}'s + {{PaymentAddress/[[phone]]}} internal slot. +
++ Internal slot + | ++ Description (non-normative) + | +
---|---|
+ [[\country]] + | ++ A country as an [[ISO3166-1]] alpha-2 code stored in its + canonical uppercase form or the empty string. For example, + "JP". + | +
+ [[\addressLine]] + | ++ A frozen array, possibly of zero length, representing an + address line. + | +
+ [[\region]] + | ++ A region as a country subdivision name or the + empty string, such as "Victoria", representing the state of + Victoria in Australia. + | +
+ [[\city]] + | ++ A city or the empty string. + | +
+ [[\dependentLocality]] + | ++ A dependent locality or the empty string. + | +
+ [[\postalCode]] + | ++ A postal code or the empty string. + | +
+ [[\sortingCode]] + | ++ A sorting code or the empty string. + | +
+ [[\organization]] + | ++ An organization or the empty string. + | +
+ [[\recipient]] + | ++ A recipient or the empty string. + | +
+ [[\phone]] + | ++ A phone number or the empty string. + | +
+ dictionary AddressInit { + DOMString country = ""; + sequence<DOMString> addressLine = []; + DOMString region = ""; + DOMString city = ""; + DOMString dependentLocality = ""; + DOMString postalCode = ""; + DOMString sortingCode = ""; + DOMString organization = ""; + DOMString recipient = ""; + DOMString phone = ""; + }; ++
+ An {{AddressInit}} is passed when + [=PaymentAddress.PaymentAddress()|constructing=] a + {{PaymentAddress}}. Its members are as follows. +
++ 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. +
++ The steps to create a `PaymentAddress` from + user-provided input are given by the following algorithm. The + algorithm takes a list |redactList|. +
++ The |redactList| optionally gives user agents the possibility to + limit the amount of personal information about the recipient that + the API shares with the merchant. +
++ For merchants, the resulting {{PaymentAddress}} 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. +
++ Postal codes in certain countries can be so specific as + to uniquely identify an individual. This being a privacy + concern, some user agents only return the part of a postal code + that they deem sufficient for a merchant to calculate shipping + costs. This varies across countries and regions, and so the + choice to redact part, or all, of the postal code is left to + the discretion of implementers in the interest of protecting + users' privacy. +
++ If "region" is not in |redactList|: +
++ In some countries (e.g., Belgium) it is uncommon for users to + include a region as part of a physical address + (even if all the regions of a country are part of [[ISO3166-2]]). + As such, when the user agent knows that the user is inputting the + address for a particular country, it might not provide a field + for the user to input a region. In such cases, the user + agent returns an empty string for both {{PaymentAddress}}'s + {{PaymentAddress/region}} attribute - but the address can still + serve its intended purpose (e.g., be valid for shipping or + billing purposes). +
- Instances of {{PaymentResponse}} are created with the [=internal - slots=] in the following table: -
-- Internal Slot - | -- Description (non-normative) - | -
---|---|
- [[\complete]] - | -- Is true if the request for payment has completed (i.e., - {{PaymentResponse/complete()}} was called, or there was a fatal - error that made the response not longer usable), or false - otherwise. - | -
- [[\request]] - | -- The {{PaymentRequest}} instance that instantiated this - {{PaymentResponse}}. - | -
- [[\retryPromise]] - | -- Null, or a {{Promise}} that resolves when a user accepts the - payment request or rejects if the user aborts the payment - request. - | -
shippingaddresschange
+ shippingoptionchange
+ payerdetailchange
+ paymentmethodchange
@@ -2481,13 +3949,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:
@@ -2636,6 +4141,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 {{PaymentAddress}} 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, address + line, dependent locality, organization, phone + number, and 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 @@ -2712,9 +4308,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 @@ -2734,6 +4404,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}} @@ -3237,8 +5059,22 @@
Where sharing of privacy-sensitive information might not be obvious to users (e.g., when [=payment handler/payment method changed @@ -3317,7 +5153,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.
+
User agents MAY impose implementation-specific limits on otherwise unconstrained inputs, e.g., to prevent denial of service attacks, to guard against running out of memory, or to work around