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.
// 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 tosetState
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.
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);
}
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.
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:
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 "Add to Home Screen"
</p>
</div>
<button className="uk-button button-minimal" onClick={() => setModalOpen(false)}>Close</button>
</div>
</div>
</Modal>
)
}
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: