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

Feat(events): add BeforeInstallPromptEvent #520

Merged
merged 2 commits into from
May 9, 2017
Merged

Feat(events): add BeforeInstallPromptEvent #520

merged 2 commits into from
May 9, 2017

Conversation

marcoscaceres
Copy link
Member

Defines BeforeInstallPromptEvent, related Dictionaries, and .prompt() method as recently discussed in the relevant bug.

@marcoscaceres
Copy link
Member Author

marcoscaceres commented Nov 7, 2016

This is what the text is based on:

const internalSlots = new WeakMap();
class BeforeInstallPromptEvent extends Event {
  constructor(typeArg, eventInit) {
    // WebIDL Guard. Not in spec, as it's all handled by WebIDL.
    if (arguments.length === 0) {
      throw new TypeError("Not enough arguments. Expected at least 1.");
    }
    const initType = typeof eventInit;
    if (arguments.length === 2 && initType !== "undefined" && initType !== "object") {
      throw new TypeError("Value can't be converted to a dictionary.");
    }
    super(typeArg, Object.assign({ cancelable: true }, eventInit));

    if (eventInit && !hasValidUserChoice(eventInit)) {
      const msg = `The provided value '${eventInit.userChoice}' is not a valid` +
        " enum value of type AppBannerPromptOutcome.";
      throw new TypeError(msg);
    }
    // End WebIDL guard.
    const internal = {
      didPrompt: false,
    };
    internal.userResponsePromise = new Promise((resolve, reject) => {
      internal.resolvePromptPromise = resolve;
      internal.rejectPromptPromise = reject;
    });
    internalSlots.set(this, internal);
    if (eventInit && eventInit.userChoice) {
      const promptResponseObject = {
        userChoice: eventInit.userChoice,
      };
      internal.resolvePromptPromise(promptResponseObject);
    }
  }

  async prompt() {
    const internal = internalSlots.get(this);
    if (!this.isTrusted) {
      const msg = ".prompt() can only called by trusted events.";
      internal.rejectPromptPromise(new DOMException(msg, "NotAllowedError"));
    } else if (!internal.didPrompt) {
      await requestInstallPrompt(this);
    }
    return internal.userResponsePromise;
  }
}

@marcoscaceres
Copy link
Member Author

There is one more medium sized PR coming tomorrow that defines the final steps.

@marcoscaceres
Copy link
Member Author

Fixed a couple of little things, so updated PR...

index.html Outdated
<pre class="idl">
[Constructor(DOMString typeArg, optional BeforeInstallPromptEventInit eventInit)]
interface BeforeInstallPromptEvent : Event {
readonly attribute Promise&lt;AppBannerPromptOutcome&gt; userChoice;
Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, forgot to remove... removing now

Copy link
Collaborator

@mgiuca mgiuca left a comment

Choose a reason for hiding this comment

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

Looks pretty good. I have some quick comments, but I haven't had a deeper look at it and I want to do that tomorrow (on my workstation where I've got the repo checked out so I can build it and look at it in HTML). Can we wait until then to land this?

index.html Outdated
AppBannerPromptOutcome userChoice;
};

dictionary UserResponseObject {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not PromptResponseObject?

Copy link
Member Author

Choose a reason for hiding this comment

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

Like it! Will rename.

index.html Outdated
<a>BeforeInstallPromptEvent.prompt</a>() method).
</p>
<p>
The <a>BeforeInstallPromptEvent</a> has three internal slots, which
Copy link
Collaborator

Choose a reason for hiding this comment

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

Drop "three"; that sort of language can get out of date without anyone noticing.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, it already bit me once.

index.html Outdated
};

dictionary BeforeInstallPromptEventInit : EventInit {
AppBannerPromptOutcome userChoice;
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 still not a fan of allowing this as an argument (I don't see why this is ever useful and it's weird that it's an outcome and not a promise). I don't remember the outcome of the discussion last time I brought it up. I want to have a closer look tomorrow.

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 feel pretty strongly about having the argument. I want a way of building these events from user-land in a way that allows .prompt() to resolve. It makes testing from user-land code much easier - and provides a complete solution, which also providing all the security assurances to the browser created events.

@marcoscaceres
Copy link
Member Author

Looks pretty good. I have some quick comments, but I haven't had a deeper look at it and I want to do that tomorrow (on my workstation where I've got the repo checked out so I can build it and look at it in HTML). Can we wait until then to land this?

Absolutely. Sorry, I should have posted a link where you can view the rendered version:
https://rawgit.com/w3c/manifest/BIP/index.html

@marcoscaceres
Copy link
Member Author

Rebased, so all the parts are now in place (be sure to "shift-refresh" if viewing the rawgit link).

Copy link
Collaborator

@mgiuca mgiuca left a comment

Choose a reason for hiding this comment

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

Some more thoughts on the algorithm definition. Sorry it took the whole day to reply. I was ... distracted.

index.html Outdated
<dfn>[[\promptOutcome]]</dfn>
</dt>
<dd>
A <a>AppBannerPromptOutcome</a> enum value, initially set to
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/A/An

index.html Outdated
<dfn>[[\userResponsePromise]]</dfn>
</dt>
<dd>
A promise, which resolves with an <a>PromptResponseObject</a>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/an/a

index.html Outdated
</li>
<li>Let <a>[[\userResponsePromise]]</a> be a newly created promise.
</li>
<li>Let <a>[[\promptOutcome]]</a> be <code>null</code>.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think promptOutcome is unnecessary.

If you search the remaining text for promptOutcome, note that it is always assigned to, never read from, except:

  1. "If [[promptOutcome]] is not null" -- that's just using it as a Boolean not an outcome value.
  2. "whose userChoice member is set to event's [[promptOutcome]]." -- promptOutcome was just set to a value immediately above, so it doesn't have to be a member of BIPE, it can just be a local variable to that algorithm.

Therefore, promptOutcome can be replaced with a Boolean. But I suspect we can simplify it further.

Copy link
Member Author

Choose a reason for hiding this comment

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

Brilliant! You are right... just updating the JS implementation. I'll try to post an update later tonight.

index.html Outdated
<li>Run the following steps <a>in parallel</a>:
<ol>
<li>If <a>[[\promptOutcome]]</a> is not <code>null</code>,
resolve <var>p</var> with <a>[[\userResponsePromise]]</a> and
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wait, "resolve p with userResponsePromise"? That means to resolve a promise with a promise. I think you mean "resolve p with promptOutcome"... but I'm trying to eliminate this (see above).

Why is p a new promise at all? Why can't we just have prompt() return userResponsePromise?

Then we can kill promptOutcome entirely.

Copy link
Member Author

Choose a reason for hiding this comment

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

Wait, "resolve p with userResponsePromise"? That means to resolve a promise with a promise. I think you mean "resolve p with promptOutcome"... but I'm trying to eliminate this (see above)

Resolving the promise with the promise unwraps the internal promise - so not to expose the internal promise... but I'm trying to simplify as you suggested. Also distracted :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh so your intention was specifically to not expose the internal promise? (i.e, every time you call prompt() it's a different promise?)

Is there a good reason to do that?

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... still in shock... back to this...

Why is p a new promise at all? Why can't we just have prompt() return userResponsePromise?

It avoids exposing the internal promise. Then .prompt() can vend promises that all get resolved when the internal one does.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh so your intention was specifically to not expose the internal promise? (i.e, every time you call prompt() it's a different promise?)

Yeah, I realized we could just do that after the discussion we had in the bug (the discussion about e.prompt() === e.prompt()). I think returning a new promise works nicely because in reality the internal promise just becomes an implementation detail. But it will be nice in the spec because it makes the behavior quite nice and clear, IMHO.

@marcoscaceres
Copy link
Member Author

marcoscaceres commented Nov 9, 2016

@mgiuca, I updated .prompt() based on your feedback. I made the following changes:

  1. Remove redundant [[\promptOutcome]]
  2. Refactored .prompt() to no longer rely on [[\promptOutcome]]
  3. Still resolving .prompt() with [[\userResponsePromise]].

I also updated the example implementation in #520 (comment)

@domenic, if you have a few mins, I would really appreciate if you could give us some feedback on: https://rawgit.com/w3c/manifest/BIP/index.html#beforeinstallpromptevent-interface

The prototype implementation on which the spec text is based: #520 (comment)

@marcoscaceres
Copy link
Member Author

err, I mean @domenic... fixed above.

@domenic
Copy link
Contributor

domenic commented Nov 9, 2016

I'm a bit unclear why the promise fulfills with an object which is just a wrapper around userChoice, instead of with userChoice directly. Maybe it's future extensibility.

All the internal slots have an extra slash in them in the rendered output.

The constructor is a bit troublesome, since events all share the same constructor logic in most implementations, namely https://dom.spec.whatwg.org/#constructing-events. Maybe that logic needs to move to the prompt method? That seems like it will change things a decent bit.

"request to present an install prompt" isn't cross-linking correctly.

When referring to internal slots, it seems better to use obj.[[slotName]] notation. Currently obj is missing, which is a bit confusing since it differs: in prompt() it should be this, whereas in "request to present an install prompt" it should be event, I think.

index.html Outdated
</dt>
<dd>
A promise, which resolves with a <a>PromptResponseObject</a>, which
represent the outcome of <a>presenting an install prompt</a>.
Copy link
Collaborator

Choose a reason for hiding this comment

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

represents

index.html Outdated
</p>
<ol>
<li>
<a>Present an install prompt</a> and <var>outcome</var> be the
Copy link
Collaborator

Choose a reason for hiding this comment

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

and let outcome be the result

index.html Outdated
<pre class="example" title="'beforeinstallprompt' in action">
window.addEventListener("beforeinstallprompt", async (event) =&gt; {
event.preventDefault();
// await e.g., user composing an email...
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is a bit confusing. I think I understand it (if the user is composing an email in the application you would wait until they finish the task before prompting). But I think that's just a bit confusing of a use case and the primary use case for this is not to avoid interrupting the user, but to present in-context UI for prompting.

So how about:

// Wait for e.g., the user to request installation from inside the app.

index.html Outdated
// await e.g., user composing an email...
await Promise.all(tasksThatPreventsInstallation);
const { userChoice } = await event.prompt();
console.info(`user selected: ${userChoice}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about "user choice was: ${uc}" because the grammar would be weird of "user selected: accepted".

@marcoscaceres
Copy link
Member Author

@domenic wrote,

I'm a bit unclear why the promise fulfills with an object which is just a wrapper around userChoice, instead of with userChoice directly. Maybe it's future extensibility.

Yeah, it's for extensibility: The (non-standard) Chrome implementation currently has additional information being returned (e.g., "platforms", which could be "Web, Play Store,"), but we are still discussing if that will be included. It's likely we will add more things.

All the internal slots have an extra slash in them in the rendered output.

Will fix.

The constructor is a bit troublesome, since events all share the same constructor logic in most implementations, namely https://dom.spec.whatwg.org/#constructing-events. Maybe that logic needs to move to the prompt method? That seems like it will change things a decent bit.

Ah, yeah. Although the .prompt() logic can probably stay the same, and I can just remove the constructor prose. The internal slots can be set as private instance properties without involvement of the constructor (i.e., they are an implementation detail in a sense).

"request to present an install prompt" isn't cross-linking correctly.

Fixing.

When referring to internal slots, it seems better to use obj.[[slotName]] notation. Currently obj is missing, which is a bit confusing since it differs: in prompt() it should be this, whereas in "request to present an install prompt" it should be event, I think.

Fixing.

@marcoscaceres
Copy link
Member Author

@mgiuca, seems you were right about not having {userChoice} be passed during construction. It's incompatible with DOM's event creation. My apologies.
whatwg/dom#372 (comment)

On the upside, makes this much simpler.

@marcoscaceres
Copy link
Member Author

marcoscaceres commented Nov 10, 2016

Ok, I've integrated all feedback.

The final thing is if we stick with resolving with PromptResponseObject (safe option if we want to add additional things in the future, like .platforms) - or we feel confident we won't and we just resolve prompt() with AppBannerPromptOutcome (i.e., "dismissed", "accepted").

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 20, 2017

(That commit 0a05f3e was a rebase.)

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 20, 2017

I just did a pass over the beforeinstallprompt spec and pushed my changes (i.e., the delta between Marcos' last commit 0a05f3e and mine, 4bb2977).

Preview on rawgit.

Kenneth, do you want to have a look?

index.html Outdated
</li>
</ol>
</li>
</ol>
<div class="issue">
Implementations may wish to show a prompt if and only if the site
Copy link
Collaborator

Choose a reason for hiding this comment

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

comma around if and only if?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think comma around "and only if" is correct.

(Why can't I do many comments together? Maybe this pull request was started so long ago that it's grandfathered out.)

index.html Outdated
<a
data-cite="DOM#dom-event-preventdefault"><code>preventDefault</code></a>)
prevents the user agent from <a data-lt="presents an install
prompt">presenting an automated install prompt</a> until a later time
Copy link
Collaborator

Choose a reason for hiding this comment

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

at a later time

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I think you misinterpreted this sentence. (Marcos wrote it and I just got confused myself now.) It's not trying to prevent prompting later. It's trying to prevent prompting now, but saying that the UA can prompt again later. I've rewritten it for clarity. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok

index.html Outdated
<dfn>[[\didPrompt]]</dfn>
</dt>
<dd>
A boolean, initially <code>false</code>. Represents if this event
Copy link
Collaborator

Choose a reason for hiding this comment

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

represents whether? maybe if is fine... just sounds weird to me :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Done.

index.html Outdated
<a>Present an install prompt</a> and let <var>outcome</var> be
the result.
</li>
<li>Let <var>responseObj</var> be a newly created
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need the Obj here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mean can we just combine these into a single step? Agree... done.

</h4>
<p>
This example shows how one might prevent an automated install
prompt from showing until the user clicks a button to install the
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need some examples to when it makes sense to delay?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mean to explain why it makes sense to use this rather than just letting it happen?

I reworded that second sentence thusly:
"In this way, the site can leave installation at the user's discretion (rather than prompting at an arbitrary time), whilst still providing a prominent UI to do so."

index.html Outdated
</p>
<pre class="example"
title="Using beforeinstallprompt to present an install button">
window.addEventListener("beforeinstallprompt", async (event) =&gt; {
Copy link
Collaborator

Choose a reason for hiding this comment

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

do you need the () around event here? is that because of async... normally you dont with one argument

Copy link
Collaborator

Choose a reason for hiding this comment

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

It isn't a syntax error (but I didn't confirm whether it works; I assume it does). Done.

index.html Outdated
installButton.disabled = false;

// Wait for the user to click the button.
await installButtonClicked;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is a bit magic where this variable comes from

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm yeah. async/await is nice but a bit magic here.

Refactored into a traditional addEventListener setup. The outer function no longer needs to be async.

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 21, 2017

I addressed those comments.

Meta-review questions:

  1. I see a bunch of people using htmltidy on their specs. I tried to use this but it complained about the special respec elements and refused to do anything. Is there a trick for getting it to work on respec documents?
  2. Do you prefer me to squash all the commits into one before landing this (or perhaps Marcos' one + my changes), or keep the long tail of git commits?

PTAL. Thanks!

@kenchris
Copy link
Collaborator

I think that squashing is nicer... But you could do one commit for Marcos work and one for your modifications

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 24, 2017

cannot use await now though (not async method...)

The inner function is async, and that's where I use await. I tested it, and it works. (The outer function does not need to be async since it just immediately does things including setting up a click handler.)

I think that squashing is nicer... But you could do one commit for Marcos work and one for your modifications

Squashed into two commits.

Also, how about my HTMLTidy question? From above: "I see a bunch of people using htmltidy on their specs. I tried to use this but it complained about the special respec elements and refused to do anything. Is there a trick for getting it to work on respec documents?"

Lastly, please wait for me to get some more people at Google to look at this before merging. Thanks!

@marcoscaceres
Copy link
Member Author

@mgiuca

I see a bunch of people using htmltidy on their specs. I tried to use this but it complained about the special respec elements and refused to do anything. Is there a trick for getting it to work on respec documents?

No, there should be no trick. Make sure you are using HTML Tidy version 5.4.0 (I think that is latest).

Then just:

tidy -config tidyconfig.txt -o index.html index.html

- Fixed links.
- Simplified logic and fixed bugs in the algorithm.
- Reworked usage example.
- Rewrote some text.
- Added an issue box about install prompts.
- Addressed other review comments.
@mgiuca
Copy link
Collaborator

mgiuca commented Apr 26, 2017

Thanks. I must've been using a very old version of tidy. (Had to build from GitHub source.)

Now tidied (and squashed that into my commit).

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 26, 2017

FYI, I wrote up a doc with the things we're thinking about on Chrome before we close this out:
Chrome change proposal

This lists the things we'll have to change in Chrome to become compliant with this spec. I'm checking with some people internally whether we're OK to make these changes.

@kenchris
Copy link
Collaborator

I cannot access that doc

@mgiuca
Copy link
Collaborator

mgiuca commented Apr 26, 2017

Sorry, fixed.

@marcoscaceres
Copy link
Member Author

@mgiuca, did you get an ok to change Blink to match? Should we merge?

@mgiuca
Copy link
Collaborator

mgiuca commented May 2, 2017

@marcoscaceres Almost... I am giving @slightlyoff one more day because I haven't heard back. Unless he objects, I think we will merge as-is. Thanks for your patience.

@marcoscaceres
Copy link
Member Author

np. No hurry.

@marcoscaceres marcoscaceres merged commit b0ad114 into gh-pages May 9, 2017
@marcoscaceres marcoscaceres deleted the BIP branch May 9, 2017 04:54
@mgiuca
Copy link
Collaborator

mgiuca commented May 9, 2017

Hey,

Good you merged 👍 :)

For the record, I am still having a back-and-forth with Alex. He had some concerns (not about the spec per se but the compatibility issues if we change Chrome). But I am happy this is finally landed. We will continue to figure out the migration course for Chrome, and if we have any issues with the spec we can open new issues instead of continuing this megathread.

THANKS!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants