-
Notifications
You must be signed in to change notification settings - Fork 162
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
Conversation
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;
}
} |
There is one more medium sized PR coming tomorrow that defines the final steps. |
8d3a5da
to
a34f0fa
Compare
Fixed a couple of little things, so updated PR... |
dce5e44
to
7a9d04d
Compare
index.html
Outdated
<pre class="idl"> | ||
[Constructor(DOMString typeArg, optional BeforeInstallPromptEventInit eventInit)] | ||
interface BeforeInstallPromptEvent : Event { | ||
readonly attribute Promise<AppBannerPromptOutcome> userChoice; |
There was a problem hiding this comment.
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
There was a problem hiding this 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not PromptResponseObject?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Absolutely. Sorry, I should have posted a link where you can view the rendered version: |
Rebased, so all the parts are now in place (be sure to "shift-refresh" if viewing the rawgit link). |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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>. |
There was a problem hiding this comment.
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:
- "If [[promptOutcome]] is not null" -- that's just using it as a Boolean not an outcome value.
- "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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@mgiuca, I updated .prompt() based on your feedback. I made the following changes:
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) |
err, I mean @domenic... fixed above. |
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 |
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>. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) => { | ||
event.preventDefault(); | ||
// await e.g., user composing an email... |
There was a problem hiding this comment.
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}`); |
There was a problem hiding this comment.
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".
@domenic wrote,
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.
Will fix.
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).
Fixing.
Fixing. |
@mgiuca, seems you were right about not having On the upside, makes this much simpler. |
Ok, I've integrated all feedback. The final thing is if we stick with resolving with |
(That commit 0a05f3e was a rebase.) |
index.html
Outdated
</li> | ||
</ol> | ||
</li> | ||
</ol> | ||
<div class="issue"> | ||
Implementations may wish to show a prompt if and only if the site |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at a later time
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
I addressed those comments. Meta-review questions:
PTAL. Thanks! |
I think that squashing is nicer... But you could do one commit for Marcos work and one for your modifications |
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.)
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! |
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.
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). |
FYI, I wrote up a doc with the things we're thinking about on Chrome before we close this out: 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. |
I cannot access that doc |
Sorry, fixed. |
@mgiuca, did you get an ok to change Blink to match? Should we merge? |
@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. |
np. No hurry. |
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! |
Defines
BeforeInstallPromptEvent
, related Dictionaries, and.prompt()
method as recently discussed in the relevant bug.