forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseInstallPrompt.ts
More file actions
87 lines (74 loc) · 3.24 KB
/
Copy pathuseInstallPrompt.ts
File metadata and controls
87 lines (74 loc) · 3.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Installing the app to the home screen.
//
// This lives in the app rather than on the landing page, and that placement is
// the whole point: a page can only be installed if IT has the manifest and
// service worker. The landing page at /OurHike/ has neither, so Chrome there
// offers "Add to Home screen", which makes a plain bookmark - it opens in a
// tab with browser chrome, has no service worker, and works offline not at
// all. It looks exactly like a successful install, which is what makes it
// dangerous: someone would carry that bookmark into the woods believing they
// had the app.
//
// `beforeinstallprompt` is Chromium-only. Safari has no install API whatsoever,
// so iOS is told what to tap instead - a button that silently does nothing is
// worse than no button on the one screen whose job is getting someone
// installed.
import { useCallback, useEffect, useState } from 'react'
export type InstallPlatform = 'installed' | 'android' | 'ios' | 'other'
/** The event Chromium fires when a page qualifies for installation. Not in
* TypeScript's DOM lib, since it is not a standard. */
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
export function isStandalone(): boolean {
return (
window.matchMedia('(display-mode: standalone)').matches ||
// Safari's own non-standard flag, the only signal iOS gives.
(window.navigator as { standalone?: boolean }).standalone === true
)
}
export function detectInstallPlatform(): InstallPlatform {
if (isStandalone()) return 'installed'
const ua = navigator.userAgent
if (/iPad|iPhone|iPod/.test(ua)) return 'ios'
// iPadOS 13+ reports itself as a Mac; the touch points give it away.
if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return 'ios'
if (/Android/.test(ua)) return 'android'
return 'other'
}
export interface InstallState {
platform: InstallPlatform
/** True only once the browser has confirmed the page really is installable,
* so the button is never shown on a promise this build cannot keep. */
canPrompt: boolean
install: () => void
}
export function useInstallPrompt(): InstallState {
const [platform, setPlatform] = useState<InstallPlatform>(detectInstallPlatform)
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(null)
useEffect(() => {
const onBeforeInstall = (event: Event) => {
// Chrome shows its own mini-infobar unless the event is prevented; the
// app asks at a moment it chooses instead.
event.preventDefault()
setDeferred(event as BeforeInstallPromptEvent)
}
const onInstalled = () => {
setDeferred(null)
setPlatform('installed')
}
window.addEventListener('beforeinstallprompt', onBeforeInstall)
window.addEventListener('appinstalled', onInstalled)
return () => {
window.removeEventListener('beforeinstallprompt', onBeforeInstall)
window.removeEventListener('appinstalled', onInstalled)
}
}, [])
const install = useCallback(() => {
if (deferred === null) return
void deferred.prompt()
void deferred.userChoice.finally(() => setDeferred(null))
}, [deferred])
return { platform, canPrompt: deferred !== null, install }
}