Skip to content

Commit 322bf76

Browse files
committed
Adds React component, config, docs
0 parents  commit 322bf76

12 files changed

+5526
-0
lines changed

.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-react", "@babel/preset-env"]
3+
}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_Store
2+
node_modules
3+
package-lock.json

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Chris Dance
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# React-iOS-PWA-prompt
2+
3+
> Polyfilling PWA prompts for iOS
4+
5+
A React component that provides a customisable Progressive Web App (PWA) prompt, matching native iOS <=12 or 13 styles for both light and dark UI modes. This behaviour is inherent on Android devices, yet does not happen on iOS. This component aims to provide a simple way to provide this functionality for websites that are PWA-ready.
6+
7+
<hr>
8+
9+
<img src="https://user-images.githubusercontent.com/11626619/65389000-18352d00-dd49-11e9-82c8-6fac25a494c8.gif" width="33%">
10+
11+
<hr>
12+
13+
## Features
14+
15+
- 🛠 Customisable delay, title and content.
16+
- ☎️ Uses LocalStorage to only notify the user once.
17+
- 📱 Detects user's iOS version and UI color scheme to provide native looking prompt
18+
19+
## Usage
20+
21+
Add `react-ios-pwa-prompt` as a dependency using `yarn add react-ios-pwa-prompt`.
22+
23+
Import into your project:
24+
25+
```
26+
import PWAPrompt from 'react-ios-pwa-prompt'
27+
```
28+
29+
Use the component:
30+
31+
```
32+
<PWAPrompt />
33+
```
34+
35+
There are also optional props that you can pass to the component:
36+
37+
- `delay` pass an integer in ms to add a delay to the prompt
38+
- `title` pass a string to customise the title of the prompt
39+
- `copy` pass a string to customise the body of the prompt
40+
41+
## Examples:
42+
43+
iOS 12:
44+
45+
<img src="https://user-images.githubusercontent.com/11626619/65392822-c902f280-dd70-11e9-8f81-a5099ca38d7f.png" width="312px">
46+
47+
iOS 13 Light:
48+
49+
<img src="https://user-images.githubusercontent.com/11626619/65392823-c902f280-dd70-11e9-9b9c-e782ec4e2721.png" width="312px">
50+
51+
iOS 13 Dark:
52+
53+
<img src="https://user-images.githubusercontent.com/11626619/65392824-c902f280-dd70-11e9-95c7-e58af3d1e71a.png" width="312px">

dist/react-ios-pwa-prompt.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "react-ios-pwa-prompt",
3+
"version": "1.0.0",
4+
"description": "A React component to prompt the user to add the app as a PWA to the home screen with native iOS styles.",
5+
"main": "./dist/react-ios-pwa-prompt.js",
6+
"scripts": {
7+
"test": "tead",
8+
"build": "webpack"
9+
},
10+
"keywords": [
11+
"pwa",
12+
"prompt",
13+
"add to home screen",
14+
"iOS",
15+
"progressive web app",
16+
"native"
17+
],
18+
"author": "Chris Dance",
19+
"license": "MIT",
20+
"private": false,
21+
"peerDependencies": {
22+
"react": ">=16.8.0",
23+
"react-dom": ">=16.8.0"
24+
},
25+
"devDependencies": {
26+
"@babel/cli": "^7.1.5",
27+
"@babel/core": "^7.1.6",
28+
"@babel/preset-env": "^7.1.6",
29+
"@babel/preset-react": "^7.0.0",
30+
"babel-loader": "^8.0.6",
31+
"babel-plugin-module-resolver": "^3.2.0",
32+
"css-loader": "^3.2.0",
33+
"node-sass": "^4.12.0",
34+
"path": "^0.12.7",
35+
"sass-loader": "^8.0.0",
36+
"style-loader": "^1.0.0",
37+
"webpack": "^4.5.0",
38+
"webpack-cli": "^3.2.1"
39+
}
40+
}

src/components/HomeScreenIcon.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
3+
const HomeScreenIcon = ({ className, modern = false }) => {
4+
if (modern) {
5+
return (
6+
<svg
7+
xmlns="http://www.w3.org/2000/svg"
8+
viewBox="0 0 578 584"
9+
className={className}
10+
>
11+
<g>
12+
<path d="M100.59 34.85c6.45-.78 12.96-.91 19.45-.98h332.94c11.63-.09 23.36.3 34.77 2.75 17.36 3.45 34.27 12.28 44.65 26.96 12.14 16.8 15.37 38.15 15.39 58.41.02 109.66 0 219.33.01 328.99.22 19.28.11 39.26-7.52 57.33A65.15 65.15 0 0 1 503 544.94c-18.02 7.33-37.81 7.53-56.96 7.31H130.03c-21.26.34-43.59-.4-62.83-10.53-13.99-7.08-24.93-19.59-30.57-34.17-5.7-14.08-7.2-29.43-7.21-44.5.01-114.02-.03-228.04.02-342.06.08-21.46 4.07-44.36 18.35-61.2 12.95-15.56 33.15-22.89 52.8-24.94m6.94 45.94c-9.22 1.6-18.36 5.99-23.91 13.75-6.82 8.89-8.21 20.5-8.32 31.37l-.01 334.1c.05 11.8 1.84 24.56 10.01 33.69 8.52 9.83 22.2 12.67 34.67 12.66 111.99.03 223.98-.01 335.97.01 13.94.28 29.68-3.25 38.05-15.41 6.46-8.87 7.84-20.2 7.93-30.88-.01-111.72.02-223.44-.01-335.16-.12-11.51-2.22-23.84-10.21-32.64-8.59-9.73-22.2-12.56-34.64-12.52-112.02-.02-224.04 0-336.06-.01-4.5.15-9.03.25-13.47 1.04z" />
13+
<path d="M270.57 161.49c9.51-11.79 31.35-10.14 38.35 3.58 2.8 5.17 3.47 11.15 3.47 16.94.02 28.89-.03 57.79.03 86.68 29.19.07 58.38-.02 87.57.04 7.49 0 15.62 1.63 21.02 7.23 5.66 5.74 6.94 14.46 5.64 22.14a20.67 20.67 0 0 1-9.46 14.22c-5.11 3.21-11.26 4.19-17.21 4.23-29.19.05-58.38-.02-87.56.04-.07 27.46.01 54.93-.05 82.39-.01 7.28-1.61 15.06-6.77 20.5-9.56 9.81-28.77 9.2-36.29-2.82-3.58-5.48-4.51-12.2-4.48-18.61-.01-27.16.01-54.31-.01-81.46-28.94-.08-57.88.02-86.81-.05-8.24-.05-17.28-2.07-22.76-8.7-5.37-5.77-5.97-14.42-4.36-21.79a20.78 20.78 0 0 1 11.16-14.11c5.83-2.99 12.53-3.29 18.95-3.23 27.93 0 55.87.02 83.8-.01.07-29.22-.01-58.44.04-87.67.02-6.85 1.37-14.05 5.73-19.54z" />
14+
</g>
15+
</svg>
16+
);
17+
}
18+
19+
return (
20+
<svg
21+
xmlns="http://www.w3.org/2000/svg"
22+
viewBox="55.99425506591797,31.989999771118164,157.7657470703125,157.7637176513672"
23+
className={className}
24+
>
25+
<path
26+
fill="#58595b"
27+
d="M90.49 32.83a54.6 54.6 0 0 1 9.55-.84c23.98.03 47.96 0 71.94.01 8.5.07 17.3 1.74 24.4 6.65 10.94 7.28 16.52 20.54 17.35 33.3.06 26.03 0 52.06.03 78.08 0 10.16-3.59 20.56-10.95 27.73-7.93 7.61-18.94 11.43-29.79 11.98-25.71.03-51.42 0-77.12.01-10.37-.11-21.01-3.77-28.17-11.48-8.22-8.9-11.72-21.29-11.73-33.21.01-23.03-.03-46.05.02-69.07-.01-9.14 1.33-18.71 6.65-26.4 6.21-9.4 16.97-14.79 27.82-16.76m38.18 41.09c-.05 10.25.01 20.5 0 30.75-9.58-.03-19.16.02-28.75-.04-2.27.08-4.98-.25-6.68 1.61-2.84 2.34-2.75 7.12.01 9.48 1.8 1.69 4.46 1.57 6.75 1.64 9.56-.04 19.12-.01 28.67-.03.02 10.24-.06 20.48.01 30.72-.14 2.66 1.36 5.4 3.95 6.3 3.66 1.66 8.52-1.13 8.61-5.23.26-10.59.02-21.2.09-31.79 9.88 0 19.76.02 29.64.01 2.74.12 5.85-.67 7.14-3.34 2.23-3.75-.61-9.34-5.08-9.29-10.57-.14-21.14-.01-31.7-.04-.01-10.25.04-20.49 0-30.74.3-3.5-2.66-7.09-6.3-6.79-3.65-.33-6.66 3.26-6.36 6.78z"
28+
/>
29+
<path
30+
fill="#fff"
31+
d="M128.67 73.92c-.3-3.52 2.71-7.11 6.36-6.78 3.64-.3 6.6 3.29 6.3 6.79.04 10.25-.01 20.49 0 30.74 10.56.03 21.13-.1 31.7.04 4.47-.05 7.31 5.54 5.08 9.29-1.29 2.67-4.4 3.46-7.14 3.34-9.88.01-19.76-.01-29.64-.01-.07 10.59.17 21.2-.09 31.79-.09 4.1-4.95 6.89-8.61 5.23-2.59-.9-4.09-3.64-3.95-6.3-.07-10.24.01-20.48-.01-30.72-9.55.02-19.11-.01-28.67.03-2.29-.07-4.95.05-6.75-1.64-2.76-2.36-2.85-7.14-.01-9.48 1.7-1.86 4.41-1.53 6.68-1.61 9.59.06 19.17.01 28.75.04.01-10.25-.05-20.5 0-30.75z"
32+
/>
33+
</svg>
34+
);
35+
};
36+
37+
export default HomeScreenIcon;

src/components/ShareIcon.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
3+
const ShareIcon = ({ className, modern = false }) => {
4+
if (modern) {
5+
return (
6+
<svg
7+
xmlns="http://www.w3.org/2000/svg"
8+
viewBox="0 0 566 670"
9+
className={className}
10+
>
11+
<g>
12+
<path d="M254.88 11.87c4.27-4.3 9.89-7.71 16.15-7.55 6.31-.38 12.1 2.92 16.43 7.26 31.09 29.88 62.07 59.88 93.14 89.79 3.34 3.16 6.65 6.67 8.06 11.16 2.08 6.74.58 14.65-4.34 19.81-7.17 7.72-20.37 8.54-28.54 1.94-2.54-2.12-4.68-4.66-6.96-7.05-18.81-20.02-37.55-40.11-56.46-60.03.55 17.91 1.83 35.81 1.63 53.74-.02 92.02.03 184.05-.03 276.07.34 11.83-10.23 22.49-22.04 22.35-12.16.66-23.5-10.13-23.22-22.36-.07-95.67-.01-191.35-.03-287.02.39-14.27 1.13-28.52 1.62-42.79-20.3 21.41-40.41 43.01-60.64 64.48-5.05 5.67-13.14 8.12-20.54 6.6a19.1 19.1 0 0 1-15.82-17.17c-1.15-6.74 2.01-13.39 6.89-17.89 31.55-30.46 63.1-60.93 94.7-91.34z" />
13+
<path d="M42.75 206.75c16.3-16.79 40.62-22.27 63.27-22.65 27.58-.23 55.17-.06 82.76-.08.03 15.28.04 30.56 0 45.84-26.26.1-52.52-.1-78.77.1-12 .13-24.97 3.47-32.95 13.03-7.83 9.01-9.65 21.41-9.76 32.95v260.05c.03 12.87-.79 26.56 5.45 38.29 5.6 11.36 17.95 17.77 30.2 18.93 4.68.41 9.37.66 14.07.57h301.95c13.6.08 28.51.64 40.18-7.54 11.16-7.5 15.81-21.41 16.03-34.34.41-14.96.06-29.93.18-44.9V276.03c-.08-11.45-1.96-23.75-9.71-32.7-8.11-9.87-21.44-13.3-33.74-13.39-26.01-.15-52.02 0-78.03-.08-.04-15.28-.04-30.57 0-45.85 25.04-.03 50.09 0 75.13-.02 12.5-.08 25.13.75 37.25 3.97 16.06 4.12 31.23 13.28 40.54 27.23 11.33 16.56 14.39 37.19 14.43 56.86.01 93.31.02 186.62-.01 279.93-.06 20.08-3.37 41.22-15.39 57.85-10.4 14.75-27.35 23.59-44.76 27.08-11.17 2.4-22.63 2.8-34.02 2.75-106.04-.02-212.07.01-318.11-.02-19.42-.22-39.71-3.35-56.14-14.38-13.33-8.77-22.53-22.87-26.84-38.09-3.69-12.35-4.57-25.31-4.55-38.13.05-93.02-.05-186.03.05-279.05.12-22.47 5.06-46.64 21.29-63.24z" />
14+
</g>
15+
</svg>
16+
);
17+
}
18+
19+
return (
20+
<svg
21+
xmlns="http://www.w3.org/2000/svg"
22+
viewBox="0,0,120,168.86000061035156"
23+
className={className}
24+
>
25+
<g fill="currentColor">
26+
<path d="M60.17 0h.02c9.34 9.43 18.62 18.91 27.9 28.39l-2.08 2.07a585.84 585.84 0 0 0-3.56 3.49c-6.09-6.29-12.26-12.5-18.35-18.79l.08 4.25c.13 28.57.11 57.14.01 85.7-2.69-.02-5.38-.01-8.06.02-.17-28.53-.16-57.07-.01-85.6l.16-4.35c-6.15 6.23-12.27 12.5-18.39 18.77l-3.56-3.51-2.08-2.05C41.56 18.93 50.8 9.4 60.17 0z" />
27+
<path d="M0 49.29h44.09c.08 2.63.15 5.26.1 7.89-12.09 0-24.19-.02-36.28 0l.05 4.14c.1 33.22.01 66.43.04 99.65h103.99c.04-33.22-.05-66.43.05-99.64l.05-4.15c-12.09-.02-24.19 0-36.28 0-.05-2.63.02-5.26.1-7.88 14.7-.02 29.39-.01 44.09-.01v119.57H0V49.29z" />
28+
</g>
29+
</svg>
30+
);
31+
};
32+
33+
//#007aff
34+
35+
export default ShareIcon;

src/index.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { Fragment, useEffect, useState } from "react";
2+
3+
import ShareIcon from "./components/ShareIcon";
4+
import HomeScreenIcon from "./components/HomeScreenIcon";
5+
6+
import "./style.scss";
7+
8+
export default ({ delay = 1000, title = undefined, copy = undefined }) => {
9+
const [hasBeenDismissed, setDismissed] = useState(
10+
localStorage.getItem("hasSeenPWAPrompt")
11+
);
12+
const [isVisible, setVisibility] = useState(!Boolean(delay));
13+
14+
useEffect(() => {
15+
if (!hasBeenDismissed && delay) {
16+
setTimeout(() => setVisibility(true), delay);
17+
}
18+
}, []);
19+
20+
if (!hasBeenDismissed) {
21+
const userAgent = window.navigator.userAgent.toLowerCase();
22+
const isiOS = () => {
23+
return /iphone|ipad|ipod/.test(userAgent);
24+
};
25+
const isInStandaloneMode = () =>
26+
"standalone" in window.navigator && window.navigator.standalone;
27+
28+
if (isiOS() && !isInStandaloneMode()) {
29+
const dismissPrompt = () => {
30+
localStorage.setItem("hasSeenPWAPrompt", true);
31+
setVisibility(false);
32+
};
33+
34+
const isiOS13 = /os 13/.test(userAgent);
35+
36+
const visibilityClass = isVisible ? "visible" : "hidden";
37+
const iOSClass = isiOS13 ? "modern" : "legacy";
38+
39+
return (
40+
<Fragment>
41+
<div
42+
className={`pwa-prompt-overlay ${visibilityClass} ${iOSClass}`}
43+
aria-label="Close"
44+
role="button"
45+
onClick={dismissPrompt}
46+
/>
47+
<div
48+
className={`pwa-prompt ${visibilityClass} ${iOSClass}`}
49+
aria-describedby="pwa-prompt-description"
50+
aria-labelledby="pwa-prompt-title"
51+
role="dialog"
52+
>
53+
<div className="pwa-prompt-header">
54+
<p id="pwa-prompt-title" className="pwa-prompt-title">
55+
{title || `Add to Home Screen`}
56+
</p>
57+
<button className="pwa-prompt-cancel" onClick={dismissPrompt}>
58+
Cancel
59+
</button>
60+
</div>
61+
<div className="pwa-prompt-body">
62+
<div className="pwa-prompt-description">
63+
<p id="pwa-prompt-description" className="pwa-prompt-copy">
64+
{copy ||
65+
`This website has app functionality. Add it to your home screen
66+
to use it in fullscreen and while offline.`}
67+
</p>
68+
</div>
69+
</div>
70+
<div className="pwa-prompt-instruction">
71+
<div className="pwa-prompt-instruction-step">
72+
<ShareIcon className="pwa-prompt-share-icon" modern={isiOS13} />
73+
<p className="pwa-prompt-copy bold">
74+
1) Press the 'Share' button
75+
</p>
76+
</div>
77+
<div className="pwa-prompt-instruction-step">
78+
<HomeScreenIcon
79+
className="pwa-prompt-home-icon"
80+
modern={isiOS13}
81+
/>
82+
<p className="pwa-prompt-copy bold">
83+
2) Press 'Add to Home Screen'
84+
</p>
85+
</div>
86+
</div>
87+
</div>
88+
</Fragment>
89+
);
90+
} else {
91+
localStorage.setItem("hasSeenPWAPrompt", true);
92+
}
93+
}
94+
95+
return null;
96+
};

0 commit comments

Comments
 (0)