diff --git a/src/components/Header/__snapshots__/test.js.snap b/src/components/Header/__snapshots__/test.js.snap
index 432f7ea..36cf455 100644
--- a/src/components/Header/__snapshots__/test.js.snap
+++ b/src/components/Header/__snapshots__/test.js.snap
@@ -39,6 +39,9 @@ exports[`Header rendering 1`] = `
+
+
+
@@ -87,6 +90,9 @@ exports[`Header rendering with no banner 1`] = `
+
+
+
diff --git a/src/components/Header/index.js b/src/components/Header/index.js
index 9ac7529..10c7558 100644
--- a/src/components/Header/index.js
+++ b/src/components/Header/index.js
@@ -6,6 +6,7 @@ import { withNamespaces, Trans } from 'react-i18next';
import GitlabIcon from 'react-feather/dist/icons/gitlab';
import LocaleSwitcher from 'components/LocaleSwitcher';
+import InstallPrompt from 'components/InstallPrompt';
import style from './style.module.css';
@@ -31,6 +32,9 @@ export const Header = ({ banner }) => (
Privacy Policy
+
+
+
diff --git a/src/components/InstallPrompt/__snapshots__/test.js.snap b/src/components/InstallPrompt/__snapshots__/test.js.snap
new file mode 100644
index 0000000..08d3585
--- /dev/null
+++ b/src/components/InstallPrompt/__snapshots__/test.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InstallPrompt rendering 1`] = `""`;
+
+exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `""`;
+
+exports[`InstallPrompt rendering after an install prompt has been requested 2`] = `
+
+
+ Add to Home Screen
+
+
+`;
diff --git a/src/components/InstallPrompt/index.js b/src/components/InstallPrompt/index.js
new file mode 100644
index 0000000..261c188
--- /dev/null
+++ b/src/components/InstallPrompt/index.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { withNamespaces, Trans } from 'react-i18next';
+
+class InstallPrompt extends React.PureComponent {
+ state = {
+ installPrompt: null
+ }
+
+ componentDidMount() {
+ window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
+ }
+
+ handleInstallPrompt = event => {
+ this.setState({
+ installPrompt: event
+ });
+ }
+
+ handleInstall = async event => {
+ event.preventDefault();
+
+ const { installPrompt } = this.state;
+
+ try {
+ installPrompt.prompt();
+ await installPrompt.userChoice;
+ }
+ catch {
+ // User cancelled install
+ }
+
+ this.setState({ installPrompt: null });
+ }
+
+ render() {
+ const { installPrompt } = this.state;
+
+ if (!installPrompt) {
+ return null;
+ }
+
+ return
+ Add to Home Screen
+ ;
+ }
+}
+
+export { InstallPrompt };
+export default withNamespaces()(InstallPrompt);
diff --git a/src/components/InstallPrompt/test.js b/src/components/InstallPrompt/test.js
new file mode 100644
index 0000000..e0fc673
--- /dev/null
+++ b/src/components/InstallPrompt/test.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { InstallPrompt } from 'components/InstallPrompt';
+
+describe('InstallPrompt', () => {
+ test('rendering', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ test('rendering after an install prompt has been requested', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+
+ component.instance().handleInstallPrompt({
+ prompt: jest.fn()
+ });
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('adding and removing event listener', () => {
+ jest.spyOn(window, 'addEventListener');
+ jest.spyOn(window, 'removeEventListener');
+
+ const component = shallow(
+
+ );
+ const handleInstallPrompt = component.instance().handleInstallPrompt;
+
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'beforeinstallprompt',
+ handleInstallPrompt);
+
+ component.unmount();
+
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'beforeinstallprompt',
+ handleInstallPrompt);
+ });
+
+ test('accepting install prompt', async () => {
+ const component = shallow(
+
+ );
+ const promptObj = {
+ prompt: jest.fn(),
+ userChoice: Promise.resolve()
+ };
+ const eventObj = { preventDefault: jest.fn() };
+
+ component.instance().handleInstallPrompt(promptObj);
+ component.find('a').simulate('click', eventObj);
+
+ // Allow async code to run
+ await new Promise(resolve => setTimeout(resolve));
+
+ expect(eventObj.preventDefault).toHaveBeenCalled();
+ expect(promptObj.prompt).toHaveBeenCalled();
+ expect(component.state('installPrompt')).toBeNull();
+ });
+
+ test('rejecting install prompt', async () => {
+ const component = shallow(
+
+ );
+ const promptObj = {
+ prompt: jest.fn(),
+ userChoice: Promise.reject()
+ };
+ const eventObj = { preventDefault: jest.fn() };
+
+ component.instance().handleInstallPrompt(promptObj);
+ component.find('a').simulate('click', eventObj);
+
+ // Allow async code to run
+ await new Promise(resolve => setTimeout(resolve));
+
+ expect(eventObj.preventDefault).toHaveBeenCalled();
+ expect(promptObj.prompt).toHaveBeenCalled();
+ expect(component.state('installPrompt')).toBeNull();
+ });
+});
diff --git a/src/globals.module.css b/src/globals.module.css
index 61b8f7b..38bf9ab 100644
--- a/src/globals.module.css
+++ b/src/globals.module.css
@@ -46,6 +46,10 @@
padding-right: var(--list-separator-width);
}
+ & li:empty {
+ display: none;
+ }
+
& li:before {
content: '//';
padding: 0 0.5rem;
@@ -71,6 +75,10 @@
padding-left: var(--list-separator-width);
}
+ & li:empty {
+ display: none;
+ }
+
& li:after {
content: '//';
padding: 0 0.5rem;
diff --git a/src/locales/en-AC.yaml b/src/locales/en-AC.yaml
index be26725..16f5bc2 100644
--- a/src/locales/en-AC.yaml
+++ b/src/locales/en-AC.yaml
@@ -65,3 +65,5 @@
DOWNLOAD PNG
"Loading...": |
LOADING...
+"Add to Home Screen": |
+ ADD TO HOME SCREEN
diff --git a/src/locales/en.yaml b/src/locales/en.yaml
index b185462..19b5905 100644
--- a/src/locales/en.yaml
+++ b/src/locales/en.yaml
@@ -65,3 +65,5 @@
Download PNG
"Loading...": |
Loading...
+"Add to Home Screen": |
+ Add to Home Screen