diff --git a/packages/flutter_service_worker/CHANGELOG.md b/packages/flutter_service_worker/CHANGELOG.md new file mode 100644 index 000000000000..d0bd041d0ff6 --- /dev/null +++ b/packages/flutter_service_worker/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial release. diff --git a/packages/flutter_service_worker/LICENSE b/packages/flutter_service_worker/LICENSE new file mode 100644 index 000000000000..7ecdec034b6d --- /dev/null +++ b/packages/flutter_service_worker/LICENSE @@ -0,0 +1,28 @@ +Copyright 2020 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_service_worker/README.md b/packages/flutter_service_worker/README.md new file mode 100644 index 000000000000..12b30db8c47a --- /dev/null +++ b/packages/flutter_service_worker/README.md @@ -0,0 +1,43 @@ +## Flutter Service Worker + +A collection of utility APIs for interacting with the Flutter service worker. + +### Setup + +The `init` method should be called in main to bootstrap the service worker API. This is safe +to call on non-web platforms. + +```dart +void main() { + serviceWorkerApi.init(); + runApp(MyApp()); +} +``` + +### Detecting a new version + +A service worker will cache the old application until the new application is downloaded and ready. To be notified when this occurs, listen for the `newVersionReady` future to resolve to a updater. You can then use `updater.reload()` to refresh to the new version. + +```dart + +serviceWorkerApi.newVersionReady.then((updater) { + print('Updating to new version'); + updater.reload(); +}); + +``` + +### Prompting for user install + +Some browsers allow displaying a notification to install the web application to home screens or start menus. This can be done by waiting for `installPromptReady` to resolve with an `installer`. Once this is done, `installer.showInstallPrompt()` can be called in response to user input, which will display a prompt. + +```dart +serviceWorkerApi.installPromptReady.then((installer) { + showVersionInstallDialog().then((bool yes) { + if (yes) { + installer.showInstallPrompt(); + } + }); +}) + +``` diff --git a/packages/flutter_service_worker/lib/flutter_service_worker.dart b/packages/flutter_service_worker/lib/flutter_service_worker.dart new file mode 100644 index 000000000000..6b6f670de8a9 --- /dev/null +++ b/packages/flutter_service_worker/lib/flutter_service_worker.dart @@ -0,0 +1,63 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'src/_io_impl.dart' if (dart.library.js) 'src/_web_impl.dart'; + +/// An API for interacting with the service worker for application caching and +/// installation. +/// +/// On platforms other than the web, this delegates to a no-op implementation. +/// +/// See also: +/// +/// * https://web.dev/customize-install/ +abstract class ServiceWorkerApi { + /// Initialize the service worker API. + /// + /// This method should be called immediately in main, before calling + /// [runApp]. + void init(); + + /// A future that resolves when the browser will allow an "add to home screen" + /// prompt to be shown. + /// + /// If the application is not compatible with a service worker installation, + /// for example by running on http instead of https, then this future + /// will never resolve. + /// + /// This installation prompt event is currently only supported on Chrome. + /// On other browsers this future will never resolve. + Future get installPromptReady; + + /// A future that resolves when a new version of the application is ready. + Future get newVersionReady; +} + +/// An handler provided when a new service worker has downloaded and activated. +abstract class UpdateResponse { + /// Reload the application. + /// + /// This can be used after [newVersionReady] completes to refresh the page + /// with the new application loaded. + void reload(); +} + +/// A handler provided when the browser indicates this application is +/// permitted to show an install prompt. +abstract class InstallResponse { + /// Trigger a prompt that allows users to install their application to the + /// device/home screen location. + /// + /// Returns a boolean that indicates whether the installation prompt was + /// accepted. + /// + /// This installation prompt event is currently only supported on Chrome. + /// On other browsers [installPromptReady] will never resolve. + Future showInstallPrompt(); +} + +/// The singleton [ServiceWorkerApi] instance. +ServiceWorkerApi get serviceWorkerApi => + _serviceWorkerApi ??= ServiceWorkerImpl(); +ServiceWorkerApi _serviceWorkerApi; diff --git a/packages/flutter_service_worker/lib/src/_io_impl.dart b/packages/flutter_service_worker/lib/src/_io_impl.dart new file mode 100644 index 000000000000..32f1d251fd9d --- /dev/null +++ b/packages/flutter_service_worker/lib/src/_io_impl.dart @@ -0,0 +1,20 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../flutter_service_worker.dart'; + +/// An unsupported implementation of the [ServiceWorkerApi] for non-web +/// platforms. +class ServiceWorkerImpl extends ServiceWorkerApi { + @override + Future get installPromptReady => Completer().future; + + @override + void init() {} + + @override + Future get newVersionReady => Completer().future; +} diff --git a/packages/flutter_service_worker/lib/src/_web_impl.dart b/packages/flutter_service_worker/lib/src/_web_impl.dart new file mode 100644 index 000000000000..db983ea1fd31 --- /dev/null +++ b/packages/flutter_service_worker/lib/src/_web_impl.dart @@ -0,0 +1,95 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library _web_impl; + +import 'dart:async'; +import 'dart:html'; +import 'dart:js'; +import 'dart:js_util'; + +import '../flutter_service_worker.dart'; + +/// An implementation of the [ServiceWorkerApi] that delegates to the JS Window +/// object. +class ServiceWorkerImpl extends ServiceWorkerApi { + @override + void init() { + window.addEventListener('beforeinstallprompt', allowInterop((Object event) { + if (_installPromptReady.isCompleted) { + return; + } + _installPrompt = JsObject.fromBrowserObject(event) + ..callMethod('preventDefault'); + _installPromptReady.complete(_WebInstallResponse(_installPrompt)); + })); + window.navigator.serviceWorker.ready + .then((ServiceWorkerRegistration registration) { + if (registration.waiting != null) { + if (!_newVersionReady.isCompleted) { + _newVersionReady.complete(_WebUpdateResponse()); + } + } + if (registration.installing != null) { + _handleInstall(registration.installing); + } + registration.addEventListener('updatefound', (_) { + _handleInstall(registration.installing); + }); + }); + } + + void _handleInstall(ServiceWorker serviceWorker) { + serviceWorker.addEventListener('statechange', (_) { + if (serviceWorker.state == 'installed') { + if (!_newVersionReady.isCompleted) { + _newVersionReady.complete(); + } + } + }); + } + + final Completer<_WebInstallResponse> _installPromptReady = + Completer<_WebInstallResponse>(); + final Completer<_WebUpdateResponse> _newVersionReady = + Completer<_WebUpdateResponse>(); + JsObject _installPrompt; + + @override + Future get installPromptReady => _installPromptReady.future; + + @override + Future get newVersionReady => _newVersionReady.future; +} + +class _WebInstallResponse extends InstallResponse { + _WebInstallResponse(this._installPrompt); + + final JsObject _installPrompt; + + @override + Future showInstallPrompt() async { + assert(_installPrompt != null, + 'The installation future needs to resolve before acceptInstallPrompt can be called'); + if (_installPrompt == null) { + throw StateError('missing installPrompt'); + } + try { + await promiseToFuture(_installPrompt.callMethod('prompt')); + } catch (err) { + throw StateError(err.toString()); + } + final String result = + await promiseToFuture(getProperty(_installPrompt, 'userChoice')); + return result == 'accepted'; + } +} + +class _WebUpdateResponse extends UpdateResponse { + @override + void reload() { + // TODO(jonahwilliams): on Safari force refresh. + window.location.reload(); + } +} diff --git a/packages/flutter_service_worker/pubspec.yaml b/packages/flutter_service_worker/pubspec.yaml new file mode 100644 index 000000000000..6478e34e1ffc --- /dev/null +++ b/packages/flutter_service_worker/pubspec.yaml @@ -0,0 +1,13 @@ +name: flutter_service_worker +description: Flutter package for integrating with the web Service Worker +homepage: https://github.com/flutter/packages/tree/master/packages/flutter_service_worker +version: 0.0.1 +authors: + - Jonah Williams + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.8.0 <3.0.0" + +dev_dependencies: + test: 1.14.3 \ No newline at end of file