w3c / manifest

Manifest for web apps
https://www.w3.org/TR/appmanifest/
Other
660 stars 162 forks source link

Add installation prompt control flow #417

Closed owencm closed 7 years ago

owencm commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

What about

engagementchange(EngagementChangeReason reason)

enum EngagementChangeReason {
  "uninstalled",
  "installed-manually",
  "install-prompt-shown",
  "install-prompt-cancelled",
  "install-prompt-accepted",
  "install-prompt-denied"
}
mounirlamouri commented 8 years ago

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: https://github.com/w3c/manifest/issues/417#issuecomment-165442341

kenchris commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 https://github.com/w3c/manifest/issues/417#issuecomment-164981366 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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

Marcos, do you mean beforeinstall or beforeinstallprompt?

marcoscaceres commented 8 years ago

On 15 Feb 2016, at 7:43 PM, Mounir Lamouri notifications@github.com wrote:

Marcos, do you mean beforeinstall or beforeinstallprompt?

Sorry, I meant beforeinstallprompt.

— Reply to this email directly or view it on GitHub.

anssiko commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

@benfredwells, this is super helpful! thanks.

anssiko commented 8 years ago

@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 commented 8 years ago

Will start on this tomorrow.

marcoscaceres commented 8 years ago

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)

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 commented 8 years ago

@owencm, @RByers, as OPs, would like to hear your thoughts on https://github.com/w3c/manifest/issues/417#issuecomment-200192679.

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

marcoscaceres commented 8 years ago

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 commented 8 years ago

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?)

mounirlamouri commented 8 years ago

Did you see the API implemented in Chrome? https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/modules/app_banner/AppBannerPromptResult.idl https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/modules/app_banner/BeforeInstallPromptEvent.idl

Maybe you could mention the reasons regarding the differences?

anssiko commented 8 years ago

@marcoscaceres Thanks for the proposal. Is the promise returned by prompt() fulfilled with "installed" when the installation actually succeeds (analogous to install event firing) or when the user clicks the "install the app" button (in which case the installation can still fail)? I feel the former would be better.

marcoscaceres commented 8 years ago

@anssiko, yes, the former. Sorry for not being clear.

marcoscaceres commented 8 years ago

@mounirlamouri asked:

Maybe you could mention the reasons regarding the differences?

Mainly, the Chrome implementation fails to handle case 1.

Also, there are a number of unclear things about the Chrome implementation. Like, why is the platform needed? Won't the platform always just match the platform you are on (e.g., As a user: I'm in Chrome on Android; I see the app banner and install the Android app... I can't install the iOS app, as I'm, you know, on Android!?).

The same applies to BeforeInstallPromptEventInit, why do you need the platform there?

Why do you need both Promise<void> prompt() and .userChoice, when prompt() can answer the question for you?

marcoscaceres commented 8 years ago

@mounirlamouri, the Chrome proposal also conflates denied and dismissed. Firefox non-modal prompts use 3 states... yes, it's a bit of a nightmare... don't get me started.

benfredwells commented 8 years ago

Like, why is the platform needed? Won't the platform always just match the platform you are on (e.g., As a user: I'm in Chrome on Android; I see the app banner and install the Android app... I can't install the iOS app, as I'm, you know, on Android!?).

In that case there are two possible platforms: web and android. With the related_applications manifest field web apps can have both related non-web apps (e.g. Android) as well as the web app itself. Both of these could be installed.

The prefer_related_applications field allows a web app to give a hint about which platform it would prefer to have installed, but UAs are under no obligation to follow that hint.

marcoscaceres commented 8 years ago

Quick update... currently working on this in the "install_event" branch. Hope to have a PR soon.

adrianhopebailie commented 8 years ago

@felquis requested support for a specific use case "Detect if the user has the app installed" which was de-prioritized in favor of implementing the install events.

Would anyone be opposed to me logging a new issue with this explicit requirement?

My rationale is that the Web Payments WG [1] is considering making app manifests a central part of how Payment Apps [2] will work and we have a similar use case.

[1] https://github.com/w3c/webpayments/ [2] https://w3c.github.io/browser-payment-api/specs/architecture.html#payment-apps

marcoscaceres commented 8 years ago

@adrianhopebailie asked:

Would anyone be opposed to me logging a new issue with this explicit requirement?

No objections. Please file a new bug and we can pick that up there.

marcoscaceres commented 8 years ago

I'm having trouble deciding what to do if the installation process fails: That is, once the user makes a choice to proceed with installation, it can fail because: out of space, security error, etc. It doesn't seem right to me to make .prompt()'s promise handle this case, as was previously suggested by @slightlyoff.

If we are going to handle it at all, I feel there should be an window.oninstallerror event handler instead.

Thoughts @mounirlamouri, @kenchris, @anssiko ?

marcoscaceres commented 8 years ago

Over on Moz's Bugzilla, @bzbarsky pointed out that BeforeInstallPromptEvent has a pretty serious issue:

So the idea is that you can only prompt if no one else has prevented you from doing so? And hence the behavior is event listener ordering dependent? :(

Boris is right and that's indeed pretty borked. @mounirlamouri, @slightlyoff, we might need to come up with some other way to deal with beforeinstallprompt.

I might separate install into its own bug, as that event should not be controversial.

marcoscaceres commented 8 years ago

I've spun off the install event to https://github.com/w3c/manifest/issues/455 and sent a PR #453. @owencm, your review would be appreciated.

mgiuca commented 8 years ago

Hi, I'd like to pick up where this discussion left off in April (since it seems we dropped the ball on the Chrome side). I've chatted with Dom, Mounir, Ben, Alex and Owen (all from Chrome) to get up to speed.

Here's the state of play wrt beforeinstallprompt (BIP) and install events:

It looks like install is going ahead and it's relatively simple and non-controversial, so I've started implementation in Chrome (I just sent an Intent to Implement email today; Chrome tracking bug).

However, we still think there's value in BIP. Last signal from Marcos on this was:

we might need to come up with some other way to deal with beforeinstallprompt.

I don't think install satisfies this requirement. We want a way to report to the website before showing the install prompt, to give the site an opportunity to suppress the banner and expose it to the user at a more convenient time (e.g., instead of it popping up in the user's face, it can provide a banner/button to install).

Now I'm trying to understand what Boris's objections are to the current implementation of BIP in Chrome. I don't really understand this:

So the idea is that you can only prompt if no one else has prevented you from doing so? And hence the behavior is event listener ordering dependent? :(

My understanding is that you can only prompt iff you've previously called preventDefault on the event. So it seems more like the "event listener ordering dependent" problem is if you have two listeners, one calls preventDefault and the other calls prompt:

window.addEventListener('beforeinstallprompt', e => e.preventDefault());
window.addEventListener('beforeinstallprompt', e => e.prompt());

Now in this particular case, it's unspecified whether the prompt is shown, depending on whether the first or second event fires first. Is that Boris's problem? Please explain if I'm mistaken.

I'm trying to figure out how serious a problem this is. On the one hand, it's trivial for a web developer to write code that depends on the delivery order of events (and we can't do anything about that). But if we really want to solve this, we could make it so that prompt() is always successful: the very existence of the BeforeInstallPromptEvent means that the UA has permitted an install prompt, so you should always be able to show the prompt if you have such an object. Calling preventDefault() on the event after calling prompt() would have no effect. Therefore, as prompt() always trumps preventDefault(), if you call both you will see the prompt regardless of the order. Does that satisfy your concerns?

On the other hand, I do find there something a bit weird about using the event object itself to do this (potentially having to store the event object around for a long time until you are ready to prompt it). Some more approaches (backwards-incompatible with the current Chrome implementation):

I hope we can get beforeinstallprompt into the spec as it provides real value and we don't want it to just sit around as a Chrome-only thing.

kenchris commented 8 years ago

I agree that the beforeinstallprompt is quite useful. Showing the prompt at the right time can really make a difference (watch the presentation by booking.com at the PWA summit: https://youtu.be/LUwdZfEW0C4?t=9m58s). It can also make the install prompt less annoying, like the way Flipkart is doing it.

Like @mgiuca I also didn't fully understand the complains.

kenchris commented 8 years ago

I think the problem with navigator.showInstallPrompt() is that way you don't necessarily know whether it will succeed or not, which means that you cannot adopt your UI in the way Flipkart does it. Of course, we could add another method for querying that, or people can set some state when receiving the beforeinstallevent event, but this way it might be a bit more disconnected than the solution currently in Chrome.

mgiuca commented 8 years ago

Yep I agree with Ken's analysis on showInstallPrompt. (Just trying to throw out some alternatives to make sure we aren't going with the current status quo just because it's already implemented.)

kenchris commented 8 years ago

I think the problem is that another script can block you from ever seeing the prompt in an undeterminable way. Having a separate method is in some ways worse, as a third party script might try calling the show method at a point the host site doesn't intent, even if it wasn't the same script calling preventDefault().

mgiuca commented 8 years ago

Are you saying there's a problem with a malicious third-party JavaScript running in your site's JS context (capturing the install event, calling preventDefault, and never prompting)? I think if you're letting untrustworthy JS code run on your site (due to XSS presumably) then it's game over already; we can't design around that scenario.

marcoscaceres commented 8 years ago

It looks like install is going ahead and it's relatively simple and non-controversial, so I've started implementation in Chrome (I just sent an Intent to Implement email today; Chrome tracking bug).

🎉

I don't think install satisfies this requiement. We want a way to report to the website before showing the install prompt, to give the site an opportunity to suppress the banner and expose it to the user at a more convenient time (e.g., instead of it popping up in the user's face, it can provide a banner/button to install.

We are all in 100% agreement with the above - and that "install" is a separate use case. We don't need to discuss it here as it's solved.

Now in this particular case, it's unspecified whether the prompt is shown, depending on whether the first or second event fires first. Is that Boris's problem?

I think it's actually the opposite - (but hopefully @bzbarsky can comment here). I think it's the opposite:

//  Script 1 - totally show it! (<script async>) 
window.addEventListener('beforeinstallprompt', e => e.prompt());
// Script 2 - whoa! don't show it! (<script async>)
window.addEventListener('beforeinstallprompt', e => e.preventDefault());

Calling preventDefault() on the event after calling prompt() would have no effect.

Ok, but this might be where you end up getting into funky race conditions depending on ordering of in which scripts are executed (using script@async): event order registration is not deterministic.

  • A global method like navigator.showInstallPrompt() that lets you explicitly ask to show an install prompt, which will succeed if you've been given a BIP event and haven't yet prompted, but fails otherwise.

I kinda like this. I also don't like the idea of having to hang onto random objects from the event.

Are we sure we can't do a flipkart here?

var wasPrompted = false; 
addEventListener("beforeinstallprompt", e => {
    e.defaultPrevented();
    wasPrompted = true;
    icon.shakeThatCutePhoneIcon();
});

icon.onclick = (e) => {
   if(icon.isShaking && wasPrompted){
      navigator.showInstallPrompt();
   }
}

A little out of left field, but we might even consider if there is overlap with the permissions API?

navigator.permissions.query({name:'install'}).then(function(result) {
   if (result.state == 'prompt') {
     navigator.permissions.request({name:'install'}).then(...)      
  }
});

I hope we can get beforeinstallprompt into the spec as it provides real value and we don't want it to just sit around as a Chrome-only thing.

Us too! I've emailed Boris and Olli Pettay (who Gecko's events model probably better than anyone). They are the ones that will need to approve if this goes into Gecko, so I'm hoping they will have good feedback.

mgiuca commented 8 years ago

I think it's the opposite:

window.addEventListener('beforeinstallprompt', e => e.prompt()); window.addEventListener('beforeinstallprompt', e => e.preventDefault());

That's the same as my example with the two registrations swapped around, right? My assumption was that the registration order doesn't matter (so this is the same as my example)... either way, the point is that as it stands now (in Chrome), it will either prompt or not prompt depending on the order the events get delivered (which may or may not be the same as the declaration order).

The question is whether it's acceptable to have a web API that behaves differently depending on event order. It doesn't seem too horrible given that I can easily write some code like this:

var pressed = False;
window.addEventListener('keydown', e => { pressed = true; });
window.addEventListener('keydown', e => {
  if (pressed)
    alert('You pressed the key');
});

Whether the alert is shown depends on the execution order of the listeners, which is unspecified. Any stateful API can be made to behave differently depending on event listener evaluation order. So I'm not really seeing the big deal of adding a new API whose behaviour can be made to depend on event listener evaluation order (really, it just means the API is stateful).

Anyway, assuming we don't want the API to be abuseable in this way...

Ok, but this might be where you end up getting into funky race conditions depending on ordering of in which scripts are executed (using script@async): event order registration is not deterministic.

My suggestion was to make prompt show the prompt regardless of whether preventDefault had been called, and conversely, make preventDefault not prevent the prompt if prompt has been explicitly called. This should mean in the above example (regardless of which order the listeners are registered or which order the UA decides to execute the listeners in) it behaves the same way.

So either way, the user will see the prompt.

Are we sure we can't do a flipkart here?

Your example captures the way I imagined a hypothetical navigator.showInstallPrompt to work. However, note that this API by itself doesn't solve the unspecified order problem:

window.addEventListener('beforeinstallprompt', e => e.preventDefault());
window.addEventListener('beforeinstallprompt', e => navigator.showInstallPrompt());

You have the same problem. So the question of whether to make BeforeInstallPromptEvent.prompt() (or navigator.showInstallPrompt) not require preventDefault is orthogonal to the question of whether it should be a method on BeforeInstallPromptEvent or navigator.

A little out of left field, but we might even consider if there is overlap with the permissions API?

I'm not really a fan of this approach because it implies the user would be prompted right away and/or that the page has to continuously poll to see whether the permission is available. I'd rather it be delivered as an event.

wanderview commented 8 years ago

FWIW, I think the aspect of onbeforeinstall that allows a site to hold the event until later to use the prompt method to be very unusual. If we could normalize that part to an API that can be called once a permission is granted, that would be great.

marcoscaceres commented 8 years ago

I'm not really a fan of this approach because it implies the user would be prompted right away and/or that the page has to continuously poll to see whether the permission is available. I'd rather it be delivered as an event.

sorry, I should have clarified the proposal.

The BIP would still be fired - but it would be a simple Event that is cancellable. It does not imply that or require to be prompted right away: the UA is still in control of the prompting, in coordination with the page.

The page would not be required to continuously poll because the Permission API notifies of changes to permission states (or else the Permission API itself would be completely broken - because then everyone would need to poll for every permission type).

The installation model currently proposed replicates much of the Permissions API's model, and I'm starting to think we should give serious consideration about retrofitting the current installation model to fit.

The only difference is that installation's initial state is "denied", then switches to "prompt" if BIP's default is prevented. And "granted" does nothing, as its been installed.