-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fine grained error recovery for fields #647
Comments
@aestes, @michelle-stripe, @jenan-stripe, @rsolomakhin and I discussed fine grained error recovery for fields during the F2F, including the ability to "retry" the payment. Rough proposal is to add enum AddressField {
"addressLine",
"city",
"country",
"dependentLocality",
"languageCode",
"organization",
"phone",
"postalCode",
"recipient",
"region",
"sortingCode"
}; Define enum PaymentErrorCode {
"invalidShippingAddress",
"invalidBillingAddress",
"unknown",
"badAddress" // "unserviceable" so hard to spell
};
[Constructor(PaymentErrorCode code, optional AddressField? field, optional DOMString message = "")]
interface PaymentError {
readonly attribute PaymentErrorCode type;
readonly attribute AddressField? AddressField;
readonly attribute DOMString message;
}; And add the following member to sequence<PaymentError> errorFields; Thus, "onshippingaddresschange": const errorField = [
new PaymentError("invalidShippingAddress", "country", "We don't ship to your country."),
new PaymentError("invalidShippingAddress", "postCode", "This postcode isn't valid."),
];
ev.updateWith({
...details,
error: "Couple of issues with the shipping address.",
errorFields,
}); Add lastly, we add let errors = [
new PaymentError("invalidShippingAddress", "addressLine", "I can't ship to PO boxes."),
new PaymentError("invalidBillingAddress", null, "Srsly? that's an abandoned house."),
];
await response.retry(...errors);
errors = checkResponse(response);
if(errors.length){
// try again... this would be recursive...
await response.retry(...errors);
} else {
const status = processPayment(response);
await response.complete(status);
} |
It might be a bit of a UX problem for UAs to have to render multiple errors that aren't |
Yeah, we need to say last one wins in the case of duplicates. |
Here is an implementation showing how it could work with async function doPaymentRequest() {
const request = new PaymentRequest(methods, details, options);
const response = await request.show();
try {
await retryUntilGood(response);
const status = await processPayment(response);
await response.complete(status);
} catch (err) {
// AbortError? user got bored retrying.
}
}
async function retryUntilGood(response) {
const errors = await validateResponse(response);
if (errors.length) {
await response.retry(...errors); // Can throw AbortError
await retryUntilGood(response);
}
} |
Fixed typo above. |
An alternative approach to allowing the user to retry a payment without introducing a new method would be to use an event like Apple Pay's interface PaymentRequestAuthorizedEvent : PaymentRequestUpdateEvent {
readonly attribute PaymentResponse response;
};
An author responds to request.onpaymentauthorized = function(event) {
let errorFields = checkResponse(event.response);
if (errors.length) {
event.updateWith(Promise.resolve({
details,
errorFields,
}));
} else {
const status = processPayment(response);
event.response.complete(status);
}
} This approach feels more natural to me than getting a new |
Third alternative - from @rsolomakhin's notes from the F2F meeting.
const response = await pr.show();
async function validateResponse(response){
const badThings = await getInvalidThings(response);
if (!badThings){
return; // yay, it’s all good!
}
// Oh noes, let’s get the user to fix bad things
await response.complete("retry", badThings);
return validateResponse(response);
}
await validateResponse(response); |
The method |
Regrettably, @mnoorenberghe's proposal would have been great if we were not already returning However, introducing the event leaves const request = new PaymentRequest();
const canContinue = new Promise(resolve => {
request.onpaymentauthorized = event => {
const errorFields = checkResponse(event.response);
if (errorFields.length) {
event.updateWith({
details,
errorFields,
});
return;
}
resolve();
};
});
// This is somewhat messy
// because now I'm holding promises instead of awaiting them :(
const showPromise = request.show();
await canContinue;
const response = await showPromise;
const status = processPayment(response);
response.complete(status); Definitely, could make the above leaner by doing what @mnoorenberghe does in his example above (do the Other cons would be having to introduce a new EventHandler interface type, as well as an attribute. While Now, API design aside, a larger problem when retrying will be figuring out the restrictions on what can actually be fixed... If the Need to think more about the above, or see how ApplePay is handling it. |
@marcoscaceres, IMHO, shouldn't |
Well, depends on #648 and if |
To be clear, redacted when the “shippingaddresschange” event fires. |
Consider supporting email, phoneNumber, etc. for retry. |
Seeking input:
Shipping address errorsLooking back over the discussion, I think having a As an alternative, I think we can just use dictionaries to achieve the same result: dictionary AddressErrors {
DOMString addressLine;
DOMString city;
DOMString country;
DOMString dependentLocality;
DOMString languageCode;
DOMString organization;
DOMString phone;
DOMString postalCode;
DOMString recipient;
DOMString region;
DOMString regionCode;
DOMString sortingCode;
};
partial dictionary PaymentDetailsUpdate {
AddressErrors shippingAddressErrors;
} Note: Note: Errors for redacted fields could be ignored by the UA (e.g., complaining about shipping address' Usage examplepr.onshippingaddresschange = ev => {
const shippingAddressErrors = {
"city": "FarmVille is not a real place.",
"postalCode": "Doesn't match a known zip code for your country.",
};
event.updateWith({ shippingAddressErrors });
} Payer-related errorsThe next class of errors deals specifically with payer related errors (name, email, phone): dictionary PayerErrors {
DOMString email;
DOMString name;
DOMString phone;
}
partial dictionary PaymentDetailsUpdate {
PayerErrors payerErrors;
} Exampleconst payerErrors = {
email: "The domain name is invalid",
phone: "Invalid format",
};
// We don't have a means to use this object yet... that will be `retry()`. Thoughts, before I spec it up? |
@marcoscaceres I like this approach - clean and lightweight. Thanks for the concrete proposal! |
This design seems reasonable from a spec/IDL perspective. Pretty nice and simple; just an extension of the existing "error" option in PaymentDetailsUpdate. |
LGTM! Thanks for getting back to this so quickly! I only have two nits:
|
Agree.
I'm thinking it might be too late... consider what happened already when we changed
I personally think this will cause confusion, as people might think they can set one or the other, but then one will trash the other, etc. Naming things is hard 😢 That reminds me, nevertheless, that maybe So, maybe we actually need: (DOMString or sequence<DOMString?>) addressLine; We could say that:
So: // The whole address is bad!
const addressErrors.addressLine = "Look, buddy, I can't ship to a PO Box!";
// Address is partially bad!
const addressErrors.addressLine = [
null,
"the second line is bad!",
null,
"the fourth line is bad!"
]; Or is that overkill? |
Sounds like overkill to me |
Ok, let's see how we go with I'll wait a few days for others to chime in, but can start drafting things up behind the scenes. |
Similarly to Apple Pay, we need need to define a means to signal which fields in the payment sheet suffer from a validation error.
The text was updated successfully, but these errors were encountered: