Repository: flutter
Issue: Flutter web - Need documentation on how to conditionally display a "Refresh. New Version is available." prompt when updated code is deployed to production.
Labels: d: api docs,customer: crowd,platform-web,P3,team-web,triaged-web

We need a foolproof way to be able to force a download of `main.dart.js` when code is updated and deployed to web live. E.g. if a user is on my flutter website and I deploy a new build and they click to a new screen, they should see new code updates without having to refresh their browser, or in the service worker js in `web/index.html`, be able to conditionally display a prompt to the user to "Refresh. New Version is available".

Google Messenger on web and other web interfaces are able to detect new code is deployed without a browser refresh and a "Refresh for new version" prompt is displayed. We need this ability via the service worker js code in `web/index.html`.

In typical html sites the foolproof way to force js/css updates, while a user is actively on your site, is to change the js/css filename reference, which forces a download of the new js/css code immediately, when the user navigates to any new screen on your site. This pattern does not require a prompt at all, but if we can't do that in flutter web, having the ability to display a refresh now prompt would be just fine.

This is a serious issue, for obvious reasons. If users are not getting updated code after a build and deploy to live hosting and we have to hope that users will press their browser cache refresh button, at some point, then there will be all sorts of problems.

It would be great for this behavior in `web/index.html` to occur by simply changing the `version` number in `pubspec.yaml`, and the `flutter build web` and deploy to hosting would guarantee the user would get new code base when they navigate to a new screen or a refresh prompt.

## This is `web/index.html`

`var serviceWorkerVersion = null;` after running `flutter build web` does get a new version number on each build, but this is not forcing the serviceWorker to refresh `main.dart.js` where all code updates should be after a new build and deploy to live hosting. Only a browser refresh where the user is required to press the browser refresh button or clear the cache does `main.dart.js` update with new deployed code.

The `console.log('Installed new service worker.');` only runs when you do a full browser refresh. This never runs after a deploy with new code to hosting without a full browser refresh. E.g. if you deploy new code to hosting and then click around without a browser refresh it will not log a new service worker is installed.

```javascript
<script>
  var serviceWorkerVersion = null;
  var scriptLoaded = false;
  function loadMainDartJs() {
    if (scriptLoaded) {
      return;
    }
    scriptLoaded = true;
    var scriptTag = document.createElement('script');
    scriptTag.src = 'main.dart.js';
    scriptTag.type = 'application/javascript';
    document.body.append(scriptTag);
  }

  if ('serviceWorker' in navigator) {
    // Service workers are supported. Use them.
    window.addEventListener('load', function () {
      // Wait for registration to finish before dropping the <script> tag.
      // Otherwise, the browser will load the script multiple times,
      // potentially different versions.
      var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
      navigator.serviceWorker.register(serviceWorkerUrl)
        .then((reg) => {
          function waitForActivation(serviceWorker) {
            serviceWorker.addEventListener('statechange', () => {
              if (serviceWorker.state == 'activated') {
                console.log('Installed new service worker.');
                loadMainDartJs();
              }
            });
          }
          if (!reg.active && (reg.installing || reg.waiting)) {
            // No active web worker and we have installed or are installing
            // one for the first time. Simply wait for it to activate.
            waitForActivation(reg.installing || reg.waiting);
          } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
            // When the app updates the serviceWorkerVersion changes, so we
            // need to ask the service worker to update.
            console.log('New service worker available.');
            reg.update();
            waitForActivation(reg.installing);
          } else {
            // Existing service worker is still good.
            console.log('Loading app from service worker.');
            loadMainDartJs();
          }
        });

      // If service worker doesn't succeed in a reasonable amount of time,
      // fallback to plaint <script> tag.
      setTimeout(() => {
        if (!scriptLoaded) {
          console.warn(
            'Failed to load app from service worker. Falling back to plain <script> tag.',
          );
          loadMainDartJs();
        }
      }, 4000);
    });
  } else {
    // Service workers not supported. Just drop the <script> tag.
    loadMainDartJs();
  }
</script>
```

## Logs

<details>
<summary>flutter doctor -v</summary>

```dart
[✓] Flutter (Channel stable, 3.0.1, on Manjaro Linux 5.4.192-1-MANJARO, locale en_US.UTF-8)
    • Flutter version 3.0.1 at /opt/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision fb57da5f94 (5 days ago), 2022-05-19 15:50:29 -0700
    • Engine revision caaafc5604
    • Dart version 2.17.1
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0-rc2)
    • Android SDK at /home/nate/Android/Sdk
    • Platform android-31, build-tools 31.0.0-rc2
    • ANDROID_HOME = /opt/android-sdk
    • ANDROID_SDK_ROOT = /home/nate/Android/Sdk
    • Java binary at: /opt/android-studio/jre/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • CHROME_EXECUTABLE = /usr/bin/google-chrome-stable

[✓] Linux toolchain - develop for Linux desktop
    • clang version 13.0.1
    • cmake version 3.23.1
    • ninja version 1.10.2
    • pkg-config version 1.8.0

[✓] Android Studio (version 2021.2)
    • Android Studio at /opt/android-studio
    • Flutter plugin version 67.1.2
    • Dart plugin version 212.5744
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] Connected device (3 available)
    • sdk gphone x86 64 arm64 (mobile) • emulator-5554 • android-x64    • Android 11 (API 30) (emulator)
    • Linux (desktop)                  • linux         • linux-x64      • Manjaro Linux 5.4.192-1-MANJARO
    • Chrome (web)                     • chrome        • web-javascript • Google Chrome 101.0.4951.64

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
```

</details>

Note: I use firebase hosting. I tried adding the following headers to my `firebase.json` to prevent caching, but neither header forces a refresh of code updates deployed live. 

```
"headers": [
  { "source":"/main.dart.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] }
]

"headers": [
  { "source":"/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] }
]
```