w3c / payment-request

Payment Request API
https://www.w3.org/TR/payment-request-1.1/
Other
482 stars 183 forks source link

Allow custom data for the active payment method to be provided to `PaymentResponse.prototype.complete` #981

Closed dcrousso closed 1 year ago

dcrousso commented 2 years ago

It'd be great for there to be a standardized way for developers/merchants to provide data back to the PaymentRequest after it's been accepted. This would allow additional information to be provided back to the underlying platform payment method handler/UI.

I was thinking it'd be something like this:

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(optional PaymentComplete result = "unknown", optional PaymentMethodData paymentMethod);
}

or maybe even just

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(optional PaymentComplete result = "unknown", optional object paymentMethodData);
}

Alternatively, we maybe might want to wrap the PaymentComplete in a new IDL dictionary (just like PaymentValidationErrors), which would also allow for more extension in the future if desired.

enum PaymentResultStatus {
  "fail",
  "success",
  "unknown"
}

dictionary PaymentResult {
    PaymentResultStatus status = "unknown";
    PaymentMethodData paymentMethod;
}

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(optional PaymentResultStatus = "unknown"); // Would we still want/need this?
  [NewObject] Promise<undefined> complete(optional PaymentResult);
}

Though (in both cases) I'm not sure if we want just a PaymentMethodData or a sequence<PaymentMethodData> so that a developer/merchant can declaratively provide data back to all supported payment methods at once instead of having to adjust the data based on which payment method is currently being used.

cyberphone commented 2 years ago

Hi @dcrousso, As one of probably very few users of PaymentRequest (for Android only since iOS does not support custom payment applications), I came up with a similar need but different solution. The retry() method may be useful but in my case there is a pretty sophisticated wallet which only uses the PaymentResponse for navigation to a success page or back to a payment selection page (due to a failed or cancelled operation).

So this particular scheme rather needed a way to talk back to the merchant with the actual authorization while remaining in the wallet UI. The return from this call is either:

In the absence of a built-in talk-back mechanism, the wallet uses a somewhat awkward OOB channel requiring the Merchant to include cookies in the PaymentRequest call.

BTW, since PaymentRequest does not come with a companion API for payment instrument enrollment , an additional task was figuring out how to do that in a convenient and secure way. It turned out that (ab)using PaymentRequest was the best solution! https://cyberphone.github.io/doc/web/calling-apps-from-the-web.pdf

nickjshearer commented 2 years ago

I prefer

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(optional PaymentComplete result = "unknown", optional PaymentMethodData paymentMethod);
}

…because we’d preserve backwards compat fairly easily.

marcoscaceres commented 2 years ago

@dcrousso, all, I'll pick this up the week of the 17th when I'm back from vacation.

marcoscaceres commented 2 years ago

Random observations:

  1. If passing the PaymentMethodData to the payment sheet via .complete() is critical for this use case, then we should do the overload (i.e., no backwards compat, force a promise rejection).
  2. If 1 is not critical, then extending the method signature with a second argument is the way to go. Having said that, it maybe safer to just do the second argument, then developers can use the same code path for new and old implementations (in theory)... second arg is just ignored in old implementations, assuming then everything works as expected.
  3. What about passing a sequence? Every PaymentResponse has an associated .methodName attribute, which is the one the user chose to perform the payment (e.g., https://apple.com/pay). Thus, using a sequence<PaymentMethodData> might be somewhat redundant, as the data being passed can only ever apply to whatever paymentResponse.methodName is.

So, with the above... I tend to land at @dcrousso second suggestion:

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(
    optional PaymentComplete result = "unknown",
    optional object paymentMethodData);
}

However, I'm worried about it being an unstructured object, so how about:

dictionary PaymentCompleteDetails {
  object data;
}

partial interface PaymentResponse {
  [NewObject] Promise<undefined> complete(
         optional PaymentComplete result = "unknown", 
         optional PaymentCompleteDetails details = {});
}
dcrousso commented 2 years ago

Yeah 1 is not critical, so 2 makes the most sense. 3 is a good point. I think having dictionary PaymentCompleteDetails is a good idea :)

cyberphone commented 2 years ago

Hi Guys, Being an application developer I don't think in IDL but in JavaScript. Anyway, would it be possible describing a possible use case a little bit more? I don't understand the flow.

Is the Merchant supposed to send down additional information to the payment handler? How would the Merchant know that such information is required without first have gotten some piece of information from the payment handler?

Pardon, if I got it all wrong.

Personally I would be extremely happy if there was a possibility adding a https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel to PaymentRequest.

marcoscaceres commented 2 years ago

Being an application developer I don't think in IDL but in JavaScript. Anyway, would it be possible describing a possible use case a little bit more? I don't understand the flow.

Sure, let me try to explain.

Let's say http://marcospay.com lets a merchant complete a payment in the following way (this example is deliberately silly, but stay with me):

// marcospay supports playing the theme song to party town in the payment sheet, so cool! 🎉🥳
paymentResponse.complete("success", {data: {"theme": "party-town"}});

Is the Merchant supposed to send down additional information to the payment handler? How would the Merchant know that such information is required without first have gotten some piece of information from the payment handler?

By virtue that you've signed up with "marcospay". Marcospay's documentation tells you about passing "party-town" and what it does.

const data = {};
switch (paymentResponse.methodName) {
   case "https://marcospay.com":
       data.theme = "party-town";
       break;
   case "https://apple.com/pay":
       data.abc123 = "something";
       break;
   /// and so on...
}
paymentResponse.complete("success", data);

Personally I would be extremely happy if there was a possibility adding a MessageChannel to PaymentRequest.

That would fundamentally change the API and we would need a whole new design. The point here is just to pass a one off bit of data when the sheet is closing/payment is completing.

cyberphone commented 2 years ago

Thanx, now it became considerably clearer!

I must though admit that I don't fully understand the motivation since a normal payment application (in a Web context NB...), would rather return with a message from the Merchant. In addition, this kind of information can be provided in https://www.w3.org/TR/payment-request/#dom-paymentmethoddata

marcoscaceres commented 2 years ago

@dcrousso, I've put up a draft at https://github.com/w3c/payment-request/pull/982

Please see the issue around exceptions... would like to discuss what to do there with you over in the pull request.

marcoscaceres commented 2 years ago

@cyberphone wrote:

I must though admit that I don't fully understand the motivation since a normal payment application (in a Web context NB...), would rather return with a message from the Merchant. In addition, this kind of information can be provided in https://www.w3.org/TR/payment-request/#dom-paymentmethoddata

The addition .data is dependent on how the payment is to .complete(): "success", "failure", "unknown". So, it may not possible to know what the .data might be before the merchant calls .complete()... otherwise, the merchant would need to pass every possible .data for every possible completion type.

That would be super ugly:

const methodData = [
  {
    supportedMethods: "https://example.com/payitforward",
    data: {
      payItForwardField: "ABC",
      "success": {theme: "party-town"},
      "failure": {theme: "sad-town"},
    },
  },
  {
    supportedMethods: "https://example.com/bobpay",
    data: {
      merchantIdentifier: "XXXX",
      bobPaySpecificField: true,
      "success": {bobThing: "fooo"},
      "failure": {other: "bar"}
    },
  },
  // and so on... this would get insanely large and redundant. 
];
cyberphone commented 2 years ago

This looks pretty hypothetical but presumably Apple has a valid use case. I guess it is secret 🙃

The need for playing a merchant specified (but still predefined) melody or something like that for "failure" is not entirely apparent.

My payment application/handler requires a much more potent mechanism as outlined in: https://github.com/w3c/payment-request/issues/981#issuecomment-996500857 It is essentially a callback which is an established extension method featured in gazillions of proprietary JS-APIs.

cyberphone commented 2 years ago

Since the "sheet" never got any traction, adding data during initialization seems like a useful method with the API "as is".

However, don't let me discourage you from introducing this feature. I'm just a grumpy old platform nerd who don't change things unless it is proven to be necessary 🤓 But it is also possible that Apple had something quite different in mind with their proposal.

This is a more realistic scenario:

const methodData = [
  {
    supportedMethods: "https://apple.com/pay",
    data: {
      // Whatever extra data needed
      "success": {theme: "party-town"},
      "failure": {theme: "sad-town"},
    }
  }
];

Using a switch statement moves the data to another place but offers (AFAICT) no added functionality.