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

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

Posted on Mar 29, 2021

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.

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!

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.

```javascript // 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 setting`useState({})`. * 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! ```javascript //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.
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. ```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); } ```
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. ```javascript 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.
Our invitation to install should be clear and instructive.
#### 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: ```javascript 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: ```javascript 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. ```javascript //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 (
Install PWA

Install Bevyho!

Install this application on your homescreen for a better experience.

Tap Add to homescreen then "Add to Home Screen"

) } ``` #### The Usage Simply import the useIsIOS hook and `InstallPWA` component and away we go! ```javascript //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 && } ... ) } 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 && }` 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

Brand experiences that folks be all like, "holy shit it's magic!"

We work with future-thinking brands, agencies and startups looking to create the next killer Brand Experience.

If that sounds like you, get in touch.

Email us

©2025 Alucrative •   Contact Us!