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

Add installation prompt control flow #417

Closed
owencm opened this issue Dec 16, 2015 · 123 comments
Closed

Add installation prompt control flow #417

owencm opened this issue Dec 16, 2015 · 123 comments

Comments

@owencm
Copy link

owencm commented Dec 16, 2015

I've heard some feedback from developers that they are very excited by encouraging users to press the 'Add to Home Screen' button in Chrome, and the equivalents in Opera etc, but that it is a problem that they can't track that event for analytics purposes.

It would be great if we could somehow expose to developers that the user has installed their web app via some UA-provided button.

We could create a new event for this, or one alternate idea would be to fire a non-cancellable BeforeInstallPrompt event which immediately resolves the userChoice promise whenever the user presses a UA-provided button to install the web app.

@RByers
Copy link

RByers commented Dec 16, 2015

What's the status of spec'ing the beforeinstallprompt event? It shipped in blink based only on the explainer which doesn't seem very precise about the current design.

Anyway it seems pretty close to what you want to me. Eg. imagine it had been named 'installopportunity' instead. Do developers care to differentiate between the UA choosing to show a banner, vs. the user explicitly finding the menu option?

@marcoscaceres
Copy link
Member

@RByers the problem with "beforeinstallprompt" is that it assumes Chrome's UI install flow. That might not match what all UAs do. For example, in Firefox, we are working on "installing" apps into the about:newtab page, which doesn't mandate any install flow.

One current solution to this problem is the use the start URL and just add a query string:

{
   "start_url": "foo/?launched-from=homescreen" 
} 

Another way of detecting if the app has been installed is if the display mode has been applied (using matchMedia).

matchMedia("(display-mode: fullscreen)").matches 

@kenchris
Copy link
Collaborator

I guess the beforeinstallprompt could be added in a non-normative section?

Changing the start_url apparently affects Service Worker caching - or so I heard.

@mounirlamouri
Copy link
Member

Using start_url or matchMedia only gives information about usage of added to homescreen web apps, not about how often they are added. Users might add web apps but never actually use them or the other way around.

Regarding the initial proposal, we could have an install event which could be fired whenever the app is installed. Not entirely a big fan of this but we kind of opened the door for this when adding beforeinstallprompt.

@RByers
Copy link

RByers commented Dec 16, 2015

Speaking from the outside (I don't have any real context / expertise here) it would be great if we could figure out how to abstract the different use cases into a single API we could all support. Eg. what about an 'installstatuschanged' event with a few possible enum values (not all of which would necessarily be applicable in all scenarios/UAs/platforms) like: "manually installed", "automatic prompt", "prompt accepted", "prompt declined", etc?

@kenchris
Copy link
Collaborator

@RByers Yes, that sounds like a good idea. I know that some people also have ideas of tying this in with search engines, so that might open up for a few other options.

@kenchris
Copy link
Collaborator

What about

engagementchange(EngagementChangeReason reason)

enum EngagementChangeReason {
  "uninstalled",
  "installed-manually",
  "install-prompt-shown",
  "install-prompt-cancelled",
  "install-prompt-accepted",
  "install-prompt-denied"
}

@mounirlamouri
Copy link
Member

Why not simply use the events? The current one (beforeinstallprompt)
combined with installed should offer the same information.

On Thu, 17 Dec 2015, at 12:41, Kenneth Rohde Christiansen wrote:

What about

engagementchange(EngagementChangeReason reason)

enum EngagementChangeReason {
  "uninstalled",
  "installed-manually",
  "install-prompt-shown",
  "install-prompt-cancelled",
  "install-prompt-accepted",
  "install-prompt-denied"
}

Reply to this email directly or view it on GitHub:
#417 (comment)

@kenchris
Copy link
Collaborator

That is an option, though you could argue that it is not a prompt in all cases (manual, from search engine?) and that doesn't cover uninstall.

@mounirlamouri
Copy link
Member

If there is no prompt, there is no beforeinstallprompt event but there is an install event.

Regarding uninstall, I'm not sure this event makes a lot of sense: would you open the web app to tell it has been uninstalled? (FWIW, Chrome wouldn't be able to implement this.)

@kenchris
Copy link
Collaborator

Ok, so if I understand you correctly, you want to standardize beforeinstallprompt and additionally add an install event? I am fine with that. You are probably right that uninstall is not that useful.

@benfredwells
Copy link
Contributor

@marcoscaceres could you explain more about Firefox's problem with onbeforeinstallprompt?

I can imagine if you're automatically installing apps into your new tab page you wouldn't need this event, is there more to it than that?

FWIW in Chrome web apps don't know how they have been launched / started, and our suggestion is to use a similar parameter in the start_url, but that seems like a different problem to what onbeforeinstallprompt was designed for.

@felquis
Copy link

felquis commented Jan 4, 2016

Today my web app uses the Chrome prompt, but it also should support Safari in iOS home screen, we created a custom modal telling the users they can save the web app to their home screen.. The problem we had is that there's no way to remove this modal once the users has added the web app to their home screen.

This isn't a problem with analytics purposes but implementation.

On Safari we have a non-standard window.navigator.standalone which is terrible, but other informations on navigator would be appreciated to answer these question.

  1. Does the user has this web app on the their home screen?
if (!('AddToHomeScreen' in window.navigator)) {
  // browser doesn't support it
  return
}

if (window.navigator.AddToHomeScreen) {
  // icon in the home screen, based on this you can take lots of decisions
} else {
  // app isn't in the home screen
  // And isn't Chrome, what about showing the user how to add this to its home screen?
}

The issue I'd have with @marcoscaceres #417 (comment) mediaMatch solution is that I could use on manifest.json "display": "browser", not only "display":"standalone".

The issue I'd have with only a installed events, is that is will be fired once, then later I'll need to save it to my localStorage, or make some other workaround to know if the user has installed it or not

I'd suggest something like this

window.addEventListener('addedToHomeScreen', handleAddedToHomeScreen)
window.navigator.addedToHomeScreen // Boolean // Easy to check for browser support

function handleAddedToHomeScreen() {
  // Send analytics convertion, this user seems to love our web app
}

@owencm
Copy link
Author

owencm commented Jan 28, 2016

It sounds to me like we're converging on introducing a new install event (subject to name bikeshedding).

@felquis - I see the desire for the boolean too, although I worry that it's overly simple since the user may have installed the site but since uninstalled it (which Chrome is unable to detect today, FWIW), or visited it now in browser mode for some reason despite having installed it previously. I suggest for now that you listen for the event and write it to localstorage, which provides basically what you want but without requiring another piece of standardization.

I suggest we go ahead with standardizing the new event. Sound good?

@marcoscaceres
Copy link
Member

Ok, install is growing on me. Basically, install translates to successfully dropping an icon + name of app somewhere.

I support what @owencm is saying: being able to check, in the sense of a boolean, if an app is installed is going to be very hard (if not impossible) to implement on various OSs... as @owencm also points out, it would need to be done async, because the user could trash the icon right after install - or at any point... if at all possible, the UA would need to somehow check if the icon it dropped on the homescreen is still present (which means at least IO or IPC somewhere).

I also understand the value of onbeforeinstall and us our implementation on Android ramps up, it will likely mean that Fennec will need this event also: however, I don't know yet how our Android UX team will want to implement the install flow for a web app (i.e., if they will need it at all, or if they will do something similar to Chrome). I hope I can answer that in the next few months.

@kenchris
Copy link
Collaborator

kenchris commented Feb 8, 2016

Let's add it then.

Does the "isInstalled" boolean have to always be 100% up to date or could it be a cached value?

@marcoscaceres
Copy link
Member

Let's add it then.

"It" being "onbeforeinstall", allowing UAs to not necessarily make us of this. This event is cancellable, does not bubble, etc. Chrome folks, let me know what you do here exactly and I'll spec it.

And "install" event, happens on successful "installation" (i.e., whatever that means to the UA, as to where it allows the user to access the "installed" web application). This event is not cancellable, does not bubble, etc.

Does the "isInstalled" boolean have to always be 100% up to date or could it be a cached value?

I say we punt on isInstalled. It just won't be reliable.

@mounirlamouri
Copy link
Member

Marcos, do you mean beforeinstall or beforeinstallprompt?

@marcoscaceres
Copy link
Member

On 15 Feb 2016, at 7:43 PM, Mounir Lamouri [email protected] wrote:

Marcos, do you mean beforeinstall or beforeinstallprompt?

Sorry, I meant beforeinstallprompt.


Reply to this email directly or view it on GitHub.

@anssiko
Copy link
Member

anssiko commented Feb 15, 2016

@marcoscaceres Chrome Platform Status suggests explainer.md is the documentation for beforeinstallprompt. LayoutTests should tell what is implemented (assuming test coverage is good, @mounirlamouri?).

Without a strong use case I'd punt e.platforms for later. It exposes the related_applications that "are provided as options in an install prompt".

@benfredwells
Copy link
Contributor

@anssiko, the layout tests have been updated for the latest changes, this one is probably the best one to look at.

The explainer.md is a little out of date, so the tests are probably a better reference for what Chrome has implemented. Specific things I noticed: accepted is returned for completed installs, not installed, and e.prompt() returns a promise, which explainer.md doesn't explain.

@marcoscaceres
Copy link
Member

@benfredwells, this is super helpful! thanks.

@anssiko
Copy link
Member

anssiko commented Feb 16, 2016

@marcoscaceres If you're looking at speccing this feature I'm happy to review and contribute.

@benfredwells Thanks for confirming the layout tests' status.

@marcoscaceres
Copy link
Member

Will start on this tomorrow.

@marcoscaceres
Copy link
Member

Proposal

Ok, so we really want to deal with 2 cases here:

  1. the user initiates the install manually.
  2. the UA initiates the install automatically.

The Chrome proposal only deals with 2, so I think that is insufficient for us to standardize on.

Case 1 - the user initiates the install manually

This action is non-cancellable, occurs independently of the application (i.e., it is not observable), but the application should still be notified that it was "installed".

Solution: install event. This simple event fires after the UA has installed the application. What "install" means is left up to the UA, but generally means that the icon and application name has been added to the homescreen (or other place where apps are installed, like about:newtab).

Case 2 - UA initiated install

(mostly the Chrome case)

  • This action must be programmatically cancellable (user may be doing something important, like driving) and shouldn't be disturbed by an install prompt. So, .preventDefault() applies.
  • It is still important to recover from the point above, so the application must be able to re-.prompt();.
  • prompt() vends a promise. The prompt's promise settles with either "installed", "denied", "dismissed".
  • the "install" event fires when installation is successful.

What do people think?

Question: should we deal with install errors or punt on them for now?

IDL

enum InstallationChoice {
  "installed",
  "dismissed",
  "denied",
};

interface BeforeInstallEvent : Event {
 Promise<InstallationChoice> prompt();
};

[NoInterfaceObject]
interface AppInstallEventsMixin {
  attribute EventHandler onbeforeinstallprompt;
  attribute EventHandler oninstall;
};
window implements AppInstallEventsMixin;

@marcoscaceres
Copy link
Member

@owencm, @RByers, as OPs, would like to hear your thoughts on #417 (comment).

@mounirlamouri, how would the above sit with you? Would you be willing to change your implementation?

@marcoscaceres
Copy link
Member

Regarding: "dismissed" vs "denied" - dismissed would be the user not explicitly saying they don't want to install the application (e.g., clicking a close button, or pressing the "esc" key). "denied" is effectively saying "no, thanks!... and don't bug me again about this site".

They are different signals and it's probably important to distinguish between them: high dismissal rates might be indicative that something is wrong with the UX (or prompting might be occurring at a bad time), for example.

@kenchris
Copy link
Collaborator

Slightly off topic: Dismissed probably will mean that it will reappear next time the site is loaded which may make some people say "no thanks" if busy with other things (ie, driving - when you really shouldn't use your phone :)). In a way, I would like a 'not now' but maybe we could do some recommendations to dismiss the install prompts at least for a while (one hour?)

@marcoscaceres
Copy link
Member

@mgiuca, I agree. I'll make some modification to match the above.

@marcoscaceres
Copy link
Member

marcoscaceres commented Oct 20, 2016

@mgiuca, I think then I need to turn didPrompt into a little state machine. The you can call prompt() as many times as you like after .preventDefault() - and those calls will silently be ignored:

window.addEventListener('beforeinstallprompt', (ev) => {
  try {
    e.prompt();  // Error: preventDefault not called
  } catch (e) {}
  e.preventDefault();
  e.prompt();
  await e.userChoice; // let's say 5 seconds later this resolves... 
});

// Second one... runs immediately after the first one  
window.addEventListener('beforeinstallprompt', (ev) => {
     e.prompt(); // Noop 
     e.prompt(); // Noop
     setTimeout()=>{
         e.prompt(); // Throws, because already consumed. 
     }, 10000); // 10 seconds later
});

@marcoscaceres
Copy link
Member

marcoscaceres commented Oct 20, 2016

@mgiuca, @dominickng - ok, hopefully N-th time lucky :)

So, I think we actually do want to throw InvalidStateError if "prompting". So this would be cover all cases discussed so far (tested with @mgiuca test above too).

  prompt() {
    if (this.isTrusted === false) {
      const msg = "Untrusted events can't call prompt().";
      throw new DOMException(msg, "NotAllowedError");
    }
    let msg = "";
    switch (internalSlots.get(this).promptState) {
      case "done":
        msg = ".prompt() has expired.";
        throw new DOMException(msg, "InvalidStateError");
      case "prompting":
        msg = "Already trying to prompt.";
        throw new DOMException(msg, "InvalidStateError");
      default:
        if (this.defaultPrevented === false) {
          msg = ".prompt() needs to be called after .preventDefault()";
          throw new DOMException(msg, "InvalidStateError");
        }
        internalSlots.get(this).promptState = "prompting";
    }

    (async function task() {
      const promptOutcome = await showInstallPrompt();
      internalSlots.get(this).promptState = "done";
      internalSlots.get(this).userChoiceHandlers.resolve(promptOutcome);
    }.bind(this)())
  }

@marcoscaceres
Copy link
Member

Sorry, pasted the wrong version ... repasted the one above.

@mgiuca
Copy link
Collaborator

mgiuca commented Oct 20, 2016

I'm not sure we need a state machine: the "prompting" and "done" states aren't both needed... they are indistinguishable except for the error message so we can merge them into one and are back with a Boolean again.

(Actually, it'd be nice to work defaultPrevented into a multi-state machine, but since it's a Boolean in the underlying platform, let's not.)

So prompt becomes:

if (didPrompt) {
  error "prompt has already been shown"
  return;
}

if (defaultPrevented) {
  error "must call preventDefault first"
  return;
}

show prompt
didPrompt = true;

And if the prompt is automatically shown, it also sets didPrompt to true.

Does it need to be more complicated than that?

@marcoscaceres
Copy link
Member

Yeah, you are right. I was over complicating it.

@marcoscaceres
Copy link
Member

@dominickng, @mgiuca, @mounirlamouri , I'm still a little bit confused by this part of the spec:

      if (this.defaultPrevented === false) {
        const msg = ".prompt() needs to be called after .preventDefault()";
        throw new DOMException(msg, "InvalidStateError");
      }

Because, you can immediately do:

ev.preventDefault();
ev.prompt().then(...);

Why don't we just treat such .prompt() calls as normal? So:

addEventListener("beforeinstallprompt", async function(ev) {
   const { userChoice } = await ev.prompt();
   console.log("user chose:", userChoice); 
} );

I'm probably missing something... but that would be nicer than throwing so many errors.

@mgiuca
Copy link
Collaborator

mgiuca commented Oct 28, 2016

Yeah I actually agree about that. I think the only argument for it is that you're supposed to use preventDefault-prompt as a pair to delay showing the banner. If you call prompt without preventDefault, it's essentially a no-op and I think we just put that error there because it's "nonsensical". But:

  1. Just because something is a no-op doesn't mean it should throw an error, and
  2. You can get around it anyway, as you pointed out, and
  3. If we add a long-wait promise to tell you what the outcome is, there is actually utility in calling prompt() without preventDefault, because you will be told what the user's choice is. (i.e., it means prompt() subsumes userChoice, and then you literally never need userChoice). This allows us to actually say userChoice is deprecated (though as discussed yesterday, killing it is hard since apparently it has a lot of usage).

So I'd say take out that error. It also doesn't break any sites if we remove it.

@marcoscaceres
Copy link
Member

I'll make the change above, but need confirmation about removing the .userChoice attribute (as the spec is currently intertwined with that) - it's something we would prefer not to implement in Gecko.

Can I get an ok that .userChoice will get deprecated in Chrome?

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 1, 2016

I asked about this on the Chrome bug tracker. I'm happy for it to go away (or be deprecated) but I want to wait for Mounir's response.

@marcoscaceres
Copy link
Member

Ok, seems @mounirlamouri is ok with the deprecation. Updating spec and ref-implementation now...

@marcoscaceres
Copy link
Member

So, playing around with the updated implementation... I'm now wondering if we need to error on when promp()ting more than once at all?

Consider, a custom BIP that has the userChoice provided. This means that the UA can just resolve like this:

// This sets the internal [[promptOutcome]] immediately: 
const bip = new BeforeInstallPromptEvent("beforeinstallprompt", {"userChoice": "accepted"});
bip.prompt().then(({userChoice}) => userChoice === "accepted");
bip.prompt().then(({userChoice}) => userChoice === "accepted");
// And so on... 

So you can call .prompt() as many times as you like. You always get the same outcome.

Now consider a real/trusted BeforeInstallPromptEvent:

event.defaultPrevented();
// Some time later
const p = event.prompt();   // 1. IPC request, internally waiting.
const p2 = event.prompt(); // 2. We are waiting, so just return a new promise and wait.  
// Wait, these just resolve to the same thing... (e.g., "accepted")
Promise.all([p1, p2]).then(results => results.every("accepted") === true);
setTimeout(()=>{
   // We can even check later what it resolved to...
   ev.prompt().then({userChoice} => console.log(userChoice))
}, 10000);

The drawback of the above is that we need to keep a queue of promises that are waiting an outcome.

I don't have a strong opinion - we can keep "[[didPrompt]]" as a guard to throw InvalidStateError if already prompting.

However, I do want to allow resolving .prompt()'s promise if the promptOutcome is already known (either ahead of time, as in the case with the user-constructed event, or after the prompt has resulted in a value).

Thoughts?

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 2, 2016

I see, so you're saying that you can call prompt many times and it will just return a bunch of promises, all of which get resolved at the same time?

The drawback of the above is that we need to keep a queue of promises that are waiting an outcome.

Can't we just have prompt() always return the same promise? In effect, it just returns the userChoice promise that we used to have as an attribute. Then surely we don't need a queue.

I think the reason for that error is that if you call prompt() after the dialog choice is made, it should not be a no-op. It should "try" to prompt again and fail because you're only allowed to do that once.

So I'd be happy for:

  1. prompt()
  2. prompt()
  3. Click Install.

To have both promises succeed, but

  1. prompt()
  2. Click Install.
  3. prompt()

to have the second prompt fail. Does that sound OK?

@marcoscaceres
Copy link
Member

I see, so you're saying that you can call prompt many times and it will just return a bunch of promises, all of which get resolved at the same time?

Yep.

Can't we just have prompt() always return the same promise? In effect, it just returns the userChoice promise that we used to have as an attribute. Then surely we don't need a queue.

You are probably right - but I need to check what the WebIDL binding layer does. I thought the call would need to return a new object for some reason.

To have both promises succeed, but ... to have the second prompt fail. Does that sound OK?

This one could just resolve with the results of whatever "Click install" was. That would still adhere to only being able to actually prompt() once: you just a promise that immediately resolves to whatever "Click install" resulted with.

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 2, 2016

This one could just resolve with the results of whatever "Click install" was. That would still adhere to only being able to actually prompt() once: you just a promise that immediately resolves to whatever "Click install" resulted with.

Yeah but it would maybe be a bit misleading to have it effectively fail silently. I don't feel super strongly about it.

@marcoscaceres
Copy link
Member

Ok, so got confirmation that it's fine to return the same promise from a method... I know, it sounds silly in hindsight that it would not be. Thanks @mgiuca for catching that out.

@marcoscaceres
Copy link
Member

This is last call for review of BeforeInstallPromptEvent: https://github.com/w3c/manifest/pull/520/files

You can still comment after we merge, but prefer comments sooner rather than later so implementation can happen.

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 22, 2016

Hi Marcos,

I spoke with Owen and Alex on Friday and we have come to an agreement that we can continue with the beforeinstallprompt API mostly as it is now (in #520), but with a few wording changes to the spec to allow UAs more freedom to experiment and change their UI.

Let me summarize what we've been talking about: we have some concern that our UI is not working; feedback from developers indicates that they want to be able to show the prompt more easily and reliably. A particular concern was that once users cancel the banner, they aren't able to have a button that shows the banner again (they have to wait for another BIP event). Maybe we want to, say, give 3 denies before we give the domain a long time penalty before prompting is allowed again. The current BIP doesn't naturally allow reprompting immediately after a deny.

We have to balance this with spammy behaviour, of course, but we want to keep the UI options open. For the reprompting case, we have 3 options:

  1. Make BIPE.prompt() callable multiple times (if the UA allows reprompting).
  2. Fire another BIPE immediately after a failed call to prompt() (if the UA wants to allow another prompt).
  3. Change the API entirely.

Since we previously decided we wanted to work within the existing BIP framework, we decided that #2 is an OK option. #1 seemed wrong because it distorts the BIPE object into this long-lived object that you can use many times.

But if we fire a BIP after a failed prompt(), isn't that misleading because it isn't actually before an install prompt... the UA has no intention of showing an install prompt even if preventDefault isn't called! So that's another thing we talked about: we want to not mandate that the UA show an install prompt after a non-cancelled BIP. Because we want the freedom to be able to give the site the ability to show a prompt without necessarily showing the prompt automatically if the BIP is not cancelled. And besides, Alex is adamant, specs shouldn't mandate UI.

So in summary:

  • The spec should not mandate that the UA displays a prompt if the BIPE is not cancelled (should be up to the UA).
    • This may require changing the language around "presents an install prompt" to say non-normatively that the UA may show some UI asking the user.
    • Even when prompt() is called, we should not mandate UI. It should non-normatively suggest that that’s what probably should happen.
  • Because a BIPE may or may not indicate that an install prompt is about to happen if you don't cancel it, we could consider adding some attributes that indicate whether or not a prompt would be shown in fallthrough. But we don't need to do this; we could also just say that if you don't want a prompt, call preventDefault, if you do want a prompt, call prompt(). If you do nothing, you get unspecified default behaviour. I am personally in favour of not adding an attribute here.
  • Allow multiple BIPEs to be thrown in a single page load -- for example if the user says No, the UA is allowed to fire the event again (either immediately or after some time passes).

Thoughts on this?

@marcoscaceres
Copy link
Member

marcoscaceres commented Nov 22, 2016

I need to give this more thought... but my initial reaction is that calling prompt() multiple times or firing multiple events doesn't feel right.

Rough sketch here, if we were to keep backwards compat with current Chrome implementation - but actually just give developers what they are asking for:

async () => {
  let userChoice;
  // We add navigator.canPromptForInstall
  // that waits for installability signal
  // Resolves to true or false, because end-user may tell UA "don't bug me with installs, ever!" 
  if (await navigator.canPromptForInstall) {
    // BIP compat layer - except, browser no longer fire this on their own!
    window.beforeintallprompt = ev => {
      if (thingsThatBlockInstall.length === 0) {
        return;
      }
      ev.preventDefault();
      await Promise.all(thingsThatBlockInstall);
      ev.prompt(); // show it... it settles "navigator.requestInstallPrompt()"
    }
    await Promise.all(thingsThatBlockInstall);
    userChoice = await navigator.requestInstallPrompt();
  }
}();

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 23, 2016

It sounds like you're suggesting we have both the global method and the existing beforeinstallprompt event.

So in the reprompt case, you're saying that we would only fire BIP once, but then you could call navigator.requestInstallPrompt many times even after the user has cancelled the first one? It just seems like bipe.prompt() is going to be redundant, so if we spec both, we might end up essentially speccing something that's deprecated. (i.e., we wouldn't recommend sites use bipe.prompt; rather just always use requestInstallPrompt.

except, browser no longer fire this on their own!

We'd still want the browser to fire this event on its own, or otherwise it would be backwards-incompatible with existing sites (which would need to now call requestInstallPrompt in order to trigger the BIP?)

@marcoscaceres
Copy link
Member

It sounds like you're suggesting we have both the global method and the existing beforeinstallprompt event.

Yes... for backwards compat. I hadn't thought this through...

So in the reprompt case, you're saying that we would only fire BIP once, but then you could call navigator.requestInstallPrompt many times even after the user has cancelled the first one?

Yeah, and then we can rate limit, etc.

It just seems like bipe.prompt() is going to be redundant, so if we spec both, we might end up essentially speccing something that's deprecated. (i.e., we wouldn't recommend sites use bipe.prompt; rather just always use requestInstallPrompt.

Yeah, essentially, bipe.prompt() would have just called .requestInstallPrompt().

We'd still want the browser to fire this event on its own, or otherwise it would be backwards-incompatible with existing sites (which would need to now call requestInstallPrompt in order to trigger the BIP?)

Yeah, but it would allow for a gradual phase out of bipe: we (browsers) make no guarantees that a site will continue to meet the criteria for an automated BIPE to be fired. Over time, we could essentially get that number right down to 0.01% or whatever, because no site will meet the criteria to fire the event. There was never a guarantee at all the BIPE would fire in the future.

@marcoscaceres
Copy link
Member

To be clear - I think this is what we want:

async function tryToInstall() {
  if (!("canPromptForInstall" in navigator) || !(await navigator.canPromptForInstall)) {
    return; // Computer says no. 
  }
  await Promise.all(thingsThatBlockInstall);
  try {
    const userChoice = await navigator.requestInstallPrompt();
    doWhateverWith(userChoice);
  } catch (err) {
    // catch in case we missed our window of opportunity,
    // or we called it too soon after page load,
    // or we called it without user interaction,
    // or we got rate-limited,
    // or an invalid state because user is doing it manually now,
    // or whatever...
  }
}
installButton.onclick = tryToInstall;

@mgiuca
Copy link
Collaborator

mgiuca commented Nov 23, 2016

OK. I remember Alex had an objection to the global method -- I don't remember whether it was just due to churn or if there was an actual objection too. I'll ask him again. If we have both APIs available that should be better??

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

No branches or pull requests