A Simple React Hook to Prompt iOS Users to Install Your Wonderful PWA.

Posted on Mar 29, 2021

Or, “HAHA, why isn’t this thing working?!”

First, a little backstory

A couple of years ago, I left the advertising agency world to focus on launching my startup, while subsidizing my income by freelancing. When I finally got around to setting up my site I decided to try my hand at React.

screen-roll

It should be easy to turn your website into a PWA, right?

Once I did that, the next obvious step was to make it an installable PWA, because the future of mobile apps is just a simple matter of adding a site manifest and some icons.

Easy right?

Small annoyance. Huge impact.

Although recent versions of iOS have better support for PWA’s, there’s still no built in prompt for users to install the app, as is present on Android. This all but defeats the purpose of an installable web app simply for the reason that iOS users won’t know they can install it!

1 L0kz9p1y L63UWM9PUb9gw

Android v. iOS: targeting a similar experience on different devices.

Anyway,

Here’s a little React hook to deliver an “Add to Home Screen,” notification to iOS users. I’m sure there’s a better way to do this, but thought I’d share what works for me.

Okay, let’s start coding.


The Hook

The whole point of this hook is to load a notification only for Safari on iOS, so we’re going to create a new file called useIsIOS.js. In this code we’re going to do a few things:

  • Check if someone is viewing our app on an iOS device.
  • Are they using mobile Safari?
  • Have they not been prompted to install the app before?

If all of the above is true, then we’ll send them a prompt to install the app and, in the background, store a hasBeenPrompted item in browser localStorage with a timestamp.

We’ll start with creating a state for isIOS with useState.

// useIsIOS.js
import {useState, useEffect} from "react";

export default function useIsIOS() {
  const [isIOS, setIsIOS] = useState({});

  useEffect(() => {
    setIsIOS(checkForIOS());
    return() => console.log("CLEANUP INSTALL PROMPT", isIOS);
  }, []);

  return isIOS;
}

This is pretty simple:

  • const[isIOS, setIsIOS] = useState({}); initializes a state — similar to setState in class function. On init, the state is an empty object by settinguseState({}).
  • Then we use useEffect to do our checks from above.

At the moment, this does nothing except for try to run the checkForIOS() function. So let’s update our file to make it work!

//useIsIOS.js
import {useState, useEffect} from "react";
import moment from "moment"; // <-- new

function checkForIOS() {
  const today = moment().toDate();
  const lastPrompt = moment(localStorage.getItem("installPrompt"));
  const days = moment(today).diff(lastPrompt, "days");

  ...
}

We’ll install moment.js npm install --save moment, then import moment into our hook. We’re using moment to set & check timestamps to see if and when a user has been invited to install our app.

Getting into our checkForIOS() function, we’ll start by setting some variables, using moment, including a timestamp for today, lastPrompt (if it exists), and finally, the number of days since we last prompted our visitor to install the app.

astro-logo-setch

A quick sketch of Astro turned logo, turned splash screen.

Looking at lastPrompt, you’ll see we’re trying to get an “installPrompt” key from the browser localStorage. If it exists, we use moment() to convert it into a Date object, otherwise it returns undefined. Perfect!

Next, have a look at days. This is where we’re checking the time difference, in days, of today compared to the last time the user was prompted to install our app. If they haven’t been prompted before, then we prompt them now!

Check for devices, browser, and OS

I tried a few different options, but this seems to be the most reliable method for identifying Safari and device types on iOS. We’re updating our checkForIOS() function with some old-school, pedantic JavaScript.

//useIsIOS.js

function checkForIOS() {
  const ua = window.navigator.userAgent;
  const webkit = !!ua.match(/WebKit/i);
  const isIPad = !!ua.match(/iPad/i);
  const isIPhone = !!ua.match(/iPhone/i)
  const isIOS = isIPad || isIPhone;
  const isSafari = isIOS && webkit && !ua.match(/CriOS/i);
}
Typical JS.

It’s important to ensure that our iOS visitor is using Safari because iOS doesn’t permit other browsers to install our awesome PWA’s!

Do we prompt?

Now that we have our timestamp and device info, we can check if we should send our website visitors a notification to install our PWA.

function checkForIOS() {
  ...

  const prompt = (isNaN(days) || days > 30) && isIOS && isSafari;

  if (prompt && "localStorage" in window) {
      localStorage.setItem("installPrompt", today);
  }

  return {isIOS, isSafari, prompt};
}

Looking at our prompt variable, we’re saying:

  • “if our visitor has no stored timestamp — isNaN(days) — or we haven’t notified them to install our PWA in over 30 days,
  • “and, they’re viewing our website on an iOS device,
  • “and, they’re using the Safari browser,
  • then set prompt to true”

Depending on whether our visitor meets all the criteria, prompt will return true or false.

And finally, if prompt is true then save the current timestamp, today, in the browser’s localStorage so we don’t continue to annoy our visitors with the notification popup to install our PWA.

prompt-zoom

Our invitation to install should be clear and instructive, without being intrusive.

One more thing

Okay this is all great, but what if our visitor has already installed our awesome PWA, and is viewing our app there, not via iOS Safari browser?

We just need to add a little bit of code:

function checkForIOS() {
  if (navigator.standalone) {
    return false;
  }
  ...
}

Is it Safari or PWA?

The navigator.standalone check is asking, “is our visitor using the mobile Safari browser, or the installed PWA?” If this check returns false then everything stops. This means visitors who’ve installed, and use, our awesome PWA, will not get the notification to install.

After all of that, here’s what our final React hook looks like:

import {useState, useEffect} from "react";
import moment from "moment";

function checkForIOS() {
    if (navigator.standalone) {
      return false;
    }
    const today = moment().toDate();
    const lastPrompt = moment(localStorage.getItem("installPrompt"));
    const days = moment(today).diff(lastPrompt, "days");
    const ua = window.navigator.userAgent;
    const webkit = !!ua.match(/WebKit/i);
    const isIPad = !!ua.match(/iPad/i);
    const isIPhone = !!ua.match(/iPhone/i)
    const isIOS = isIPad || isIPhone;
    const isSafari = isIOS && webkit && !ua.match(/CriOS/i);

    const prompt = (isNaN(days) || days > 30) && isIOS && isSafari;

    if (prompt && "localStorage" in window) {
      localStorage.setItem("installPrompt", today);
    }

    return {isIOS, isSafari, prompt};
}

export default function useIsIOS() {
    const [isIOS, setIsIOS] = useState({});

    useEffect(() => {
      setIsIOS(checkForIOS());
      return() => console.log("CLEANUP INSTALL PROMPT", isIOS);
    }, []);

    return isIOS;
}

The Modal

Here’s where we make our notification. In my case, I had to use a custom useModal hook for my purposes, but there are a ton of useModal hook examples out there, so choose whatever suits you best.

//InstallPWA.js
import React, { useEffect } from "react";
import Modal from "../Modal";
import { useModal } from "../Hooks/useModal";

import "./PWA.css";
import image from "../../images/LogoMark.png";
import share from "../../images/AppleShare.png";

export const InstallPWA = ({...props}) => {
    const [modalOpen, setModalOpen, toggleModal] = useModal();

    useEffect(
        () => {
          setModalOpen(true)
        }, []
    )
    return (
          <Modal
              isActive={modalOpen}
              className="notification-card">
              <div className="uk-container uk-container-small uk-flex uk-flex-middle uk-flex-center uk-height-1-1">
                  <div style={{maxWidth: "400px"}} className="uk-card uk-card-default uk-card-body">
                      <div style={{marginTop: "-50px"}} className="uk-text-center">
                          <img
                              src={image}
                              className="uk-border-rounded"
                              height="72"
                              width="72"
                              alt="Install PWA"
                              />
                      </div>
                      <div className="uk-margin-top uk-text-center">
                          <h3>Install Bevyho!</h3>
                      </div>
                      <p className="uk-h4 uk-text-muted uk-text-center uk-margin-remove-top">Install this application on your homescreen for a better experience.</p>
                      <div className="uk-text-center">
                          <p className="uk-text-small">
                          Tap
                          <img
                              src={share}
                              style={{margin: "auto 4px 8px"}}
                              className="uk-display-inline-block"
                              alt="Add to homescreen"
                              height="20"
                              width="20"
                              />
                          then &quot;Add to Home Screen&quot;
                          </p>
                      </div>
                      <button className="uk-button button-minimal" onClick={() => setModalOpen(false)}>Close</button>
                  </div>
              </div>
          </Modal>
      )
  }
My modal code. Feel free to take, or use your own. :-)

The Usage

Simply import the useIsIOS hook and InstallPWA component and away we go!

//Home.js or whatever
import React from "react";
import useIsIOS from "../Hooks/useIsIOS";
import {InstallPWA} from "../InstallPWA";

const Home = props => {
    const { prompt } = useIsIOS();

    return (
        ...
        {prompt && <InstallPWA />}
        ...
    )
}
export default Home;

In this example, we’re loading our install prompt in the home page component with const { prompt } = useIsIOS();

Doing this provides the prompt = true/false state to our component. Awesome, right?

So now, we can easily load our notification prompt with:

{prompt && <InstallPWA />}

This effectively says, “If our visitor is using an iOS device, and they are using mobile Safari, and they haven’t been prompted in the last 30 days, and they’re not visiting our site from our installed PWA, then show them a prompt to “Add to Home Screen”.

Finally, a thank you,

For reading all the way to the bottom of the page. I hope this was helpful to at least one person. Feel free to comment with questions, revelations, insults, etc.

Here’s a link to the source code on GitHub:

MichaelLisboa/react-portfolio-site

https://github.com/MichaelLisboa/react-portfolio-site

Let's do great things

Get in touch to talk about your next project.

Create experiences that people love.

I work with future-thinking brands, agencies and startups looking to create the next killer product or campaign.

If that sounds like you, get in touch.

Message me

Email me