Skip to content

Instantly share code, notes, and snippets.

@rikukissa
Last active May 6, 2024 11:52
Show Gist options
  • Save rikukissa/cb291a4a82caa670d2e0547c520eae53 to your computer and use it in GitHub Desktop.
Save rikukissa/cb291a4a82caa670d2e0547c520eae53 to your computer and use it in GitHub Desktop.
React Hook prompting the user to "Add to homescreen" ๐Ÿ  #PWA #React
title slug createdAt language preview
React Hook prompting the user to "Add to homescreen"
react-hook-prompting-the-user-to-add
2018-11-29T20:35:02Z
en
Simple React Hook for showing the user a custom "Add to homescreen" prompt.

React Hook for showing custom "Add to homescreen" prompt

Demo:
Twitter

Simple React Hook for showing the user a custom "Add to homescreen" prompt.

const [prompt, promptToInstall] = useAddToHomescreenPrompt();

Listens for beforeinstallprompt event, which notifies you when the browser would have shown the default dialog, intercepts it and lets you take over and show the prompt when ever you please.

Browser support and requirements

Add to Home Screen

Browser support is still quite lacking. At the time of writing, only Chrome (Desktop + Android) is supported.

Implementation

import * as React from "react";

interface IBeforeInstallPromptEvent extends Event {
  readonly platforms: string[];
  readonly userChoice: Promise<{
    outcome: "accepted" | "dismissed";
    platform: string;
  }>;
  prompt(): Promise<void>;
}

export function useAddToHomescreenPrompt(): [
  IBeforeInstallPromptEvent | null,
  () => void
] {
  const [prompt, setState] = React.useState<IBeforeInstallPromptEvent | null>(
    null
  );

  const promptToInstall = () => {
    if (prompt) {
      return prompt.prompt();
    }
    return Promise.reject(
      new Error(
        'Tried installing before browser sent "beforeinstallprompt" event'
      )
    );
  };

  React.useEffect(() => {
    const ready = (e: IBeforeInstallPromptEvent) => {
      e.preventDefault();
      setState(e);
    };

    window.addEventListener("beforeinstallprompt", ready as any);

    return () => {
      window.removeEventListener("beforeinstallprompt", ready as any);
    };
  }, []);

  return [prompt, promptToInstall];
}

Example component

import * as React from "react";
import { useAddToHomescreenPrompt } from "./useAddToHomescreenPrompt";

export function ExampleComponent() {
  const [prompt, promptToInstall] = useAddToHomescreenPrompt();
  const [isVisible, setVisibleState] = React.useState(false);

  const hide = () => setVisibleState(false);

  React.useEffect(
    () => {
      if (prompt) {
        setVisibleState(true);
      }
    },
    [prompt]
  );

  if (!isVisible) {
    return <div />;
  }

  return (
    <div onClick={hide}>
      <button onClick={hide}>Close</button>
      Hello! Wanna add to homescreen?
      <button onClick={promptToInstall}>Add to homescreen</button>
    </div>
  );
}
@OleksandrDanylchenko
Copy link

OleksandrDanylchenko commented Mar 21, 2021

I ended up wrapping it in a context.

Provider/Consumer

// PromptToInstallProvider.tsx
import React, { createContext, useEffect, useContext, useCallback } from 'react';
import { Children, IBeforeInstallPromptEvent, PromptCtx } from './types';

const PromptToInstall = createContext<PromptCtx>({deferredEvt: null});

export function PromptToInstallProvider(props: Children) {
  const [deferredEvt, setDeferredEvt] = React.useState<IBeforeInstallPromptEvent | null>(
    null,
  );

  const hidePrompt = useCallback(() => {
    setDeferredEvt(null);
  }, []);

  useEffect(() => {
    const ready = (e: IBeforeInstallPromptEvent) => {
      e.preventDefault();
      setDeferredEvt(e);
    };

    window.addEventListener('beforeinstallprompt', ready as any);

    return () => {
      window.removeEventListener('beforeinstallprompt', ready as any);
    };
  }, []);

  return (
    <PromptToInstall.Provider value={{deferredEvt, hidePrompt}}>
      {props.children}
    </PromptToInstall.Provider>
  );
}

export function usePromptToInstall() {
  const ctx = useContext(PromptToInstall);
  if (!ctx) {
    throw new Error('Cannot use usePromptToInstall() outside <PromptToInstallProvider />');
  }
  return ctx;
}

Example

import React from 'react';
import { usePromptToInstall } from 'shared/providers/propmptToInstall';

export default function InstallPrompt() {
  const { deferredEvt, hidePrompt } = usePromptToInstall();

  if (!deferredEvt) {
    return null;
  }
  return (
      <span>
        <p>Install app to your device?</p>
        <div>
          <button onClick={deferredEvt.prompt}>Yes</button>
          <button onClick={hidePrompt}>No, hide message</button>
        </div>
      </span>
  );
}

Types used

// types.ts
export interface IBeforeInstallPromptEvent extends Event {
  readonly platforms: string[];
  readonly userChoice: Promise<{
    outcome: 'accepted' | 'dismissed';
    platform: string;
  }>;
  prompt(): Promise<void>;
}
export type PromptCtx = {
  deferredEvt: IBeforeInstallPromptEvent | null;
  hidePrompt?:() => void;
};
export type Children = {
  children: ReactElement | ReactElement[] | string | null;
};

Always getting TypeError: Failed to execute 'prompt' on 'BeforeInstallPromptEvent': Illegal invocation, when I'm trying to use deferredEvt.prompt, event if the functions is defined. What can be the problem?

@mwmcode
Copy link

mwmcode commented Mar 21, 2021

Weird! This error (Illegal invocation) seems to refer to the context in which the function is being executed (gathered from this SO answer).

I just tried it again on Chrome browser (Mac OS) and it worked
The only difference I have is

const onInstall = () => {
    if (deferredEvt?.prompt) {
      deferredEvt.prompt();
      invalidateEvt(); 
    } else if (BROWSER.isSafariMobile) {
      setShowIOSBox(true);
    }
};

If you have a minimal reproduceable codebase, I can look into it further

@francescogior
Copy link

Hi, I added a listener for the appinstalled event and made the hook return a third boolean flag

import * as React from "react";

interface IBeforeInstallPromptEvent extends Event {
  readonly platforms: string[];
  readonly userChoice: Promise<{
    outcome: "accepted" | "dismissed";
    platform: string;
  }>;
  prompt(): Promise<void>;
}

export function useAddToHomescreenPrompt(): [
  IBeforeInstallPromptEvent | null,
  () => void,
  boolean
] {
  const [
    promptable,
    setPromptable,
  ] = React.useState<IBeforeInstallPromptEvent | null>(null);

  const [isInstalled, setIsInstalled] = React.useState(false);

  const promptToInstall = () => {
    if (promptable) {
      return promptable.prompt();
    }
    return Promise.reject(
      new Error(
        'Tried installing before browser sent "beforeinstallprompt" event'
      )
    );
  };

  React.useEffect(() => {
    const ready = (e: IBeforeInstallPromptEvent) => {
      e.preventDefault();
      setPromptable(e);
    };

    window.addEventListener("beforeinstallprompt", ready as any);

    return () => {
      window.removeEventListener("beforeinstallprompt", ready as any);
    };
  }, []);

  React.useEffect(() => {
    const onInstall = () => {
      setIsInstalled(true);
    };

    window.addEventListener("appinstalled", onInstall as any);

    return () => {
      window.removeEventListener("appinstalled", onInstall as any);
    };
  }, []);

  return [promptable, promptToInstall, isInstalled];
}
import * as React from "react";
import { useAddToHomescreenPrompt } from "./useAddToHomescreenPrompt";
import styled from "@emotion/styled";

const ButtonElement = styled.button({
  // style
});

export function AddToHomescreenButton() {
  const [promptable, promptToInstall, isInstalled] = useAddToHomescreenPrompt();

  return promptable && !isInstalled ? (
    <ButtonElement onClick={promptToInstall}>INSTALL APP</ButtonElement>
  ) : null;
}

@ankitchauhan-aka
Copy link

ankitchauhan-aka commented Nov 11, 2021

If I'm not mistaken the event is triggered once onload so you can only use that effect in your root app component if you have different routes. (which is no big deal since we can pass prompt and promptinstall as props to child components)

Using in footer component which is only available at some pages and prompt only happens if I reload page (sometimes fails even after page reload). Any more hints?

@tasandberg
Copy link

Thanks for this gist! For anyone that wants to avoid the any type:

React.useEffect(() => {
    const ready = (e: IBeforeInstallPromptEvent) => {
      e.preventDefault()
      setState(e)
    }

    window.addEventListener(
      "beforeinstallprompt",
      ready as EventListenerOrEventListenerObject
    )

    return () => {
      window.removeEventListener(
        "beforeinstallprompt",
        ready as EventListenerOrEventListenerObject
      )
    }
 }, [])

@Anhht1808
Copy link

On the computer everything is fine, but on both Android and iOS phones it doesn't work

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