From 265e6db2bd69f928c33572572ada57dd6c5329da Mon Sep 17 00:00:00 2001 From: Pete LePage Date: Tue, 23 Jun 2020 16:10:40 -0400 Subject: [PATCH] Adds install button to Squoosh --- src/components/intro/index.tsx | 84 ++++++++++++++++++++++++- src/components/intro/missing-types.d.ts | 32 ++++++++++ src/components/intro/style.scss | 20 ++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/components/intro/missing-types.d.ts diff --git a/src/components/intro/index.tsx b/src/components/intro/index.tsx index c9ca9536..753643af 100644 --- a/src/components/intro/index.tsx +++ b/src/components/intro/index.tsx @@ -47,11 +47,27 @@ interface Props { } interface State { fetchingDemoIndex?: number; + deferredPrompt?: BeforeInstallPromptEvent; + installSource?: String; } export default class Intro extends Component { - state: State = {}; + state: State = { + deferredPrompt: undefined, + installSource: undefined, + }; private fileInput?: HTMLInputElement; + private installButton?: HTMLButtonElement; + + constructor() { + super(); + + // Listen for beforeinstallprompt events, indicating Squoosh is installable. + window.addEventListener('beforeinstallprompt', this.onBeforeInstallPromptEvent); + + // Listen for the appinstalled event, indicating Squoosh has been installed. + window.addEventListener('appinstalled', this.onAppInstalled); + } @bind private resetFileInput() { @@ -90,6 +106,62 @@ export default class Intro extends Component { } } + @bind + private onBeforeInstallPromptEvent(event: BeforeInstallPromptEvent) { + // Don't show the mini-infobar on mobile + event.preventDefault(); + + // Save the beforeinstallprompt event so it can be called later. + this.setState({ deferredPrompt: event }); + + // Log the event. + ga('send', 'event', 'pwa-install', 'available'); + + // Make the install button visible + this.installButton!.style.display = 'inline-block'; + } + + @bind + private async onInstallClick(event: Event) { + // Get the deferred beforeinstallprompt event + const deferredPrompt = this.state.deferredPrompt; + + // If there's no deferred prompt, bail. + if (!deferredPrompt) return; + + // Set the install source as the intro install button. + const installSource = 'introInstallButton'; + this.setState({ installSource }); + + // Show the browser install prompt + deferredPrompt.prompt(); + + // Wait for the user to accept or dismiss the install prompt + const response = await deferredPrompt.userChoice; + + // Get the outcome and log it + const outcome = response.outcome; + ga('send', 'event', 'pwa-install', installSource, outcome); + + // If the prompt was dismissed, clear the installSource. + if (outcome === 'dismissed') { + this.setState({ installSource: undefined }); + } + } + + @bind + private onAppInstalled() { + // If install button is visible, hide it. + const installButton = this.installButton; + if (installButton) { + installButton.style.display = 'none'; + } + + // Try to get the install, if it's not set, use 'browser' + const source = this.state.installSource || 'browser'; + ga('send', 'event', 'pwa-install', 'installed', source); + } + render({ }: Props, { fetchingDemoIndex }: State) { return (
@@ -132,6 +204,16 @@ export default class Intro extends Component { )}
+
+ +
  • View the code
  • Report a bug
  • diff --git a/src/components/intro/missing-types.d.ts b/src/components/intro/missing-types.d.ts new file mode 100644 index 00000000..8c794d4e --- /dev/null +++ b/src/components/intro/missing-types.d.ts @@ -0,0 +1,32 @@ +/** + * The BeforeInstallPromptEvent is fired at the Window.onbeforeinstallprompt handler + * before a user is prompted to "install" a web site to a home screen on mobile. + */ +interface BeforeInstallPromptEvent extends Event { + + /** + * Returns an array of DOMString items containing the platforms on which the event was dispatched. + * This is provided for user agents that want to present a choice of versions to the user such as, + * for example, "web" or "play" which would allow the user to chose between a web version or + * an Android version. + */ + readonly platforms: Array; + + /** + * Returns a Promise that resolves to a DOMString containing either "accepted" or "dismissed". + */ + readonly userChoice: Promise<{ + outcome: 'accepted' | 'dismissed', + platform: string + }>; + + /** + * Allows a developer to show the install prompt at a time of their own choosing. + * This method returns a Promise. + */ + prompt(): Promise; +} + +interface WindowEventMap { + "beforeinstallprompt": BeforeInstallPromptEvent; +} diff --git a/src/components/intro/style.scss b/src/components/intro/style.scss index 0d776195..58b83692 100644 --- a/src/components/intro/style.scss +++ b/src/components/intro/style.scss @@ -170,6 +170,26 @@ --color: #fff; } +.install-button { + composes: unbutton from '../../lib/util.scss'; + + &:hover, + &:focus { + background: #f5f5f5; + } + + background: #fff; + border: 1px solid #e8e8e8; + + margin-top: 1em; + + align-items: center; + padding: 14px; + font-size: 1.3rem; + + display: none; +} + .related-links { display: flex; padding: 0;