w3c / payment-request

Payment Request API
https://www.w3.org/TR/payment-request/
Other
488 stars 135 forks source link

Response timeout could leave page/user in confused state. #737

Open marcoscaceres opened 6 years ago

marcoscaceres commented 6 years ago

From Gecko bug, spec says:

After the payment request has been accepted and the PaymentResponse returned to the caller but before the caller calls complete() the payment request user interface remains in a pending state. At this point the user interface ought not offer a cancel command because acceptance of the payment request has been returned. However, if something goes wrong and the developer never calls complete() then the user interface is blocked.

For this reason, implementations MAY impose a timeout for developers to call complete(). If the timeout expires then the implementation will behave as if complete() was called with no arguments.

Problem is that the you have a race condition:

  1. the promise from fetch("/make-payment", {body: response.toJSON()}) may take longer to resolve than the payment sheet being presented (e.g., 5 seconds in Browser X).
  2. As the payment sheet is shutting down due to a timeout, fetch() promise resolves successfully (the user has been charged!).
  3. The merchant is now in an awkward position, having to inform the user that the payment actually went through... but unable to use the payment sheet to do so (i.e.,.complete() now just returns a rejected promise).

We might need a new event to notify the merchant if the sheet has shut down on them. Otherwise, they will try to .complete() and response will just return a rejected promise.

@domenic, @zkoch, @aestes, @adrianba, all, thoughts?

marcoscaceres commented 6 years ago

Hacky workaround:

const response = await request.show();
// sheet is waiting for complete
const didPaySucceed = await fetch("/pay", {method: "POST", body: response.toJSON()});
//... bit's taking ages... so sheet shuts down or user hits "ESC"! 
// finally, we get "didPaySucceed" result.
try {
  // oh crap, invalid state error! The sheet was closed! 
  await request.complete("success");
} catch(err){
  // site: we got this, show success or error.
  displayCompleteResult(didPaySucceed); 
}
ianbjacobs commented 6 years ago

A new event seems reasonable.

marcoscaceres commented 6 years ago

More examples and work arounds at: https://bugzilla.mozilla.org/show_bug.cgi?id=1447773#c13

But a simple event gets us to:

const payResponse = await request.show();
const response = await fetch("/process-payment/", { body: response.toJSON() }) 
response.ontimeout = ev => {
  // can show a progress indicator or whatever...
  showInPagePaymentProgress(response);
});
const result = await response;
await response.complete(result)
adrianhopebailie commented 6 years ago

Could we allow complete() to accept a Promise.

The Promise returned by complete() could resolve/reject based on that Promise (and to the same value).

A developer with long running login that they must perform (even a chain of async calls) can wrap these behind a Promise and pass to complete(). If complete() is passed a promise it should never timeout.