Skip to content
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

Dont block UI if .updateWith() not called (closes #589) #591

Merged
merged 18 commits into from
Sep 8, 2017

Conversation

marcoscaceres
Copy link
Member

@marcoscaceres marcoscaceres commented Aug 22, 2017

index.html Outdated
@@ -2417,6 +2422,9 @@
<li>If <var>event</var>'s <a>isTrusted</a> attribute is false, then
then <a>throw</a> a "<a>InvalidStateError</a>" <a>DOMException</a>.
</li>
<li>If <var>event</var>.<a>[[\didUpdate]]</a> is true, then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this would happen if someone calls updateWith(p), p settles, and then someone calls updateWith(q), right?

Whereas the next line would happen if someone calls updateWith(p), and before p settles, someone calls updateWith(q)?

If I got that right, then I'd suggest adding quick examples or notes inside these steps re-stating the above.

Copy link
Member Author

@marcoscaceres marcoscaceres Aug 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this would happen if someone calls updateWith(p), p settles, and then someone calls updateWith(q), right?

I was thinking of this case:

request.onaddresschange  = ev => { 
   Promise.resolve({...details})
     // next tick, so [[didUpdate]] is now true. Throws InvalidStateError.
     .then(ev.updateWith.bind(ev));
}

Whereas the next line would happen if someone calls updateWith(p), and before p settles, someone calls updateWith(q)?

Correct. It also prevents someone holding a reference to event and calling .updateWith(p) after p has settled. The problem in the current spec is that [[\waitForUpdate]] and request.[[updating]] are both reset to false in step 14 of updateWith() - so theoretically, a developer could have just waited like this:

request.onaddresschange  = async ev => {
   const p =  Promise.resolve({...details});
   ev.updateWith(p);
   await p;  // [[waitForUpdate]] and request.[[updating]] are both reset to `false`! 
   // [[didUpdate]] is true, this now throws
   const q = Promise.resolve({...details});
   ev.updateWith(q);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed first case above...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, added example above.

index.html Outdated
<li>
<a>Fire an event</a> named <var>name</var> at <var>request</var>
using <a>PaymentRequestUpdateEvent</a>.
<li>Let <var>event</var> be the result of <a>fire an event</a> named
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work. Fire an event returns a boolean, not an event.

You should instead:

  • Let event be the result of creating an event using Event.
  • Initialize event's type attribute to name
  • Dispatch event at target.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work. Fire an event returns a boolean, not an event.

Argh, I should have remembered that. I've used that boolean before elsewhere.

index.html Outdated
<var>event</var>.<a>[[\waitForUpdate]]</a> is true, disable any part
of the user interface that could cause another update event to be
fired. Wait for <var>event</var>.<a>updateWith()</a> method's
<var>detailsPromise</var> to settle.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

", then re-enable the relevant parts of user interface", I think.

Maybe this should be a series of nested steps under the "If ... is true".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.updateWith() controls re-enabling the user interface, in step 14. It might be redundant to re-state it here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, in that case, agreed.

</p>
<aside class="note">
<p>
If a developer wants to update the payment request, then they
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just noticing that this repeats text from slightly above. Let's delete the above text, as it feels pretty out of place anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted the one above.

index.html Outdated
};

// ✅ Good - UI will wait.
request.onshippingaddresschange = async ev =&gt; {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No async here

index.html Outdated

// ✅ Good - UI will wait.
request.onshippingaddresschange = async ev =&gt; {
// This is fine, of course.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"of course" is a bit aggressive and implies the reader understands what's going on. You shouldn't really say "of course" while teaching someone something like this, IMO.

index.html Outdated
// ❌ Bad - this won't work!
request.onshippingaddresschange = async ev =&gt; {
// `await` goes to next tick, so [[didUpdate]] is now true.
const details = await Promise.resolve({ ...details, ...updatedProps });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do await getNewDetails(oldDetails) to mirror the below.

index.html Outdated
<a>[[\didUpdate]]</a> becomes true and <a>updateWith()</a>
will throw.
</p>
<pre class="example" title="how to use `updateWith()` correctly.">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backticks don't work in example titles. Respec bug? Or just remove it for now?

Same below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to add support for this in ReSpec, so will leave it for now and we will get this for free soon.

index.html Outdated
</p>
<pre class="example" title="can only call `upadteWith()` once">
// ❌ Bad - calling updateWith() doesn't work!
request.addEventListener("shippingaddresschange", async ev =&gt; {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no async

index.html Outdated
<pre class="example" title="how to use `updateWith()` correctly.">
// ❌ Bad - this won't work!
request.onshippingaddresschange = async ev =&gt; {
// `await` goes to next tick, so [[didUpdate]] is now true.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To distinguish this from the last example, we should say something like "await goes to next tick, and updateWith() was not called before that, so [[didUpdate]] gets set to true".

Although that calls into question whether didUpdate is the right name, since nothing actually updated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know... "didUpdate" is not a great name... I wanted something that denoted "consumed" or "expired" or "this can't be used again". I'm open to suggestions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could flip it and go with "canUpdate" or "updatePermitted" and turn it off after the opportunity to update is lost or has already been taken.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although that calls into question whether didUpdate is the right name, since nothing actually updated.

Rethinking this: it actually did update... but instead of using the developer's details, the sheet used the details that were provided by the payment sheet itself. So maybe updatedUI might be better.

@dlongley, I'd prefer not to flip it. I don't want to rewrite it :(

index.html Outdated
const p = Promise.resolve({ ...details });
ev.updateWith(p);
await p;
// 💥 [[didUpdate]] is now true, throws "InvalidStateError".
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To distinguish this from the first example, we can say "the update has now completed, so [[didUpdate]] gets set to true; this throws "InvalidStateError".

index.html Outdated
"!DOM#concept-event-create">creating an event</a> using
<a>PaymentRequestUpdateEvent</a>.
</li>
<li>Initialize <var>event</var>'s type attribute to <var>name</var>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally link type, or at least <code>ify it.

index.html Outdated
<var>event</var>.<a>[[\waitForUpdate]]</a> is true, disable any part
of the user interface that could cause another update event to be
fired. Wait for <var>event</var>.<a>updateWith()</a> method's
<var>detailsPromise</var> to settle.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.updateWith()'s detailsPromise is pretty terrible. Can we store it in an internal slot?

Actually, what is the point of this "wait" step? Nothing happens after it. I think it should just be removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. I wasn't sure if it would be clear. But if one follows the updateWith() path, it provides all the steps there.

@zkoch
Copy link
Contributor

zkoch commented Aug 25, 2017

Is there a TL;DR for what we're trying to accomplish here? It's not obvious to me. :)

@marcoscaceres
Copy link
Member Author

marcoscaceres commented Aug 26, 2017

@zkoch, tldr:

  1. Don't block the payment sheet if the developer doesn't call .updateWith().
  2. Don't allow developers to keep recycling the event (e.g., keep a reference to a shipping change event, and then use it to update the the payment sheet when an option change event happens - see code below).

===

Note to self, add this test:

var methods = [{ supportedMethods: ["basic-card"] }];
var details = {
  total: { label: "", amount: { currency: "USD", value: "1" } },
};
var shippingOptions = [
  {
    id: "standard",
    label: "🚛 Ground Shipping (2 days)",
    amount: { currency: "USD", value: "5.00" },
    selected: true,
  },
  {
    id: "drone",
    label: "🚀 Drone Express (2 hours)",
    amount: { currency: "USD", value: "25.00" },
  },
];
details.shippingOptions = shippingOptions;
var r = new PaymentRequest(methods, details, { requestShipping: true });
r.addEventListener(
  "shippingaddresschange",
  e => {
    ev = e;
    ev.updateWith(details);
    var listener = ev2 => {
      var newDetails = { ...details };
      newDetails.total.label = "FAIL" + Math.random();
      ev.updateWith(newDetails);
    };
    r.addEventListener("shippingoptionchange", listener);
    r.addEventListener("shippingaddresschange", listener);
  },
  { once: true }
);
r.show();

Copy link
Collaborator

@domenic domenic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one remaining nit; please don't wait on another round of review before merging :).

index.html Outdated
<a>PaymentRequestUpdateEvent</a>.
</li>
<li>Initialize <var>event</var>'s <a data-cite="!DOM#dom-event-type">
type</a> attribute to <var>name</var>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be code-ified and linked? :)

@adrianba
Copy link
Contributor

I think the current spec already blocks this. The waitForUpdate slot is essentially equivalent to the new slot introduced. To avoid re-use of events, we should remove step 14, part 1, which sets waitForUpdate back to false.

@marcoscaceres
Copy link
Member Author

I think the current spec already blocks this. The waitForUpdate slot is essentially equivalent to the new slot introduced. To avoid re-use of events, we should remove step 14, part 1, which sets waitForUpdate back to false.

Nice!

@marcoscaceres
Copy link
Member Author

@adrianba, I removed the new slot as you suggested. Adding appropriate tests and see if this works. Will carry over domenic's R+ if it works ok.

@marcoscaceres
Copy link
Member Author

Rebased.

@marcoscaceres
Copy link
Member Author

Moved test requirement to WPT repo.
web-platform-tests/wpt#7288

I've got this mostly tested locally. Tests will hopefully land in the next few days.

I feel confident merging this now having tested it.

@marcoscaceres marcoscaceres merged commit 6bc1125 into gh-pages Sep 8, 2017
@marcoscaceres marcoscaceres deleted the cancel_flag branch September 8, 2017 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants