File size: 2,643 Bytes
ef6e870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { browser } from '$app/environment';
import { MEDIA_QUERIES } from '$lib/constants';

/**
 * iOS UA token detection.
 *
 * iPadOS 13+ ships a desktop macOS UA, so 'iPad' is no longer present in it;
 * a Macintosh UA combined with touch support is treated as an iPad instead.
 * Third-party iOS browsers (Chrome, Firefox, Edge) and in-app WKWebViews all
 * run on WKWebView and emit their own tokens (CriOS/FxiOS/EdgiOS/GSA) instead
 * of the trailing 'Safari/' the Safari app keeps.
 */
const UA_PATTERNS = {
	IOS_PHONE: /iPhone|iPod/,
	MACINTOSH: /Macintosh/,
	SAFARI: /Safari/,
	WEBVIEW_IOS: /CriOS|FxiOS|EdgiOS|GSA/
} as const;

interface DeviceContext {
	/** Any iOS/iPadOS device, regardless of which app or browser embeds the page. */
	isIOSDevice: boolean;
	/** The Safari browser app on iOS, excluding other iOS browsers and WKWebViews. */
	isIOSSafari: boolean;
	/** Any WKWebView context on iOS: in-app browsers, embedded web views, and the
	 *  third-party iOS browsers (all of which share the WKWebView engine). */
	isWKWebView: boolean;
	/** PWA standalone mode: the page was launched from the home screen icon. */
	isStandalone: boolean;
}

const SERVER_DEFAULT: DeviceContext = {
	isIOSDevice: false,
	isIOSSafari: false,
	isWKWebView: false,
	isStandalone: false
};

function detect(): DeviceContext {
	if (!browser) return SERVER_DEFAULT;

	const ua = navigator.userAgent;
	const isTouch = navigator.maxTouchPoints > 0;

	const isIOSDevice = UA_PATTERNS.IOS_PHONE.test(ua) || (UA_PATTERNS.MACINTOSH.test(ua) && isTouch);

	// Safari keeps 'Safari/' in the UA; non-Safari iOS browsers emit their own
	// token instead. WKWebView typically omits 'Safari/' entirely.
	const hasSafariToken = UA_PATTERNS.SAFARI.test(ua) && !UA_PATTERNS.WEBVIEW_IOS.test(ua);
	const isIOSSafari = isIOSDevice && hasSafariToken;
	const isWKWebView = isIOSDevice && !hasSafariToken;

	// navigator.standalone is the legacy iOS-only flag (deprecated but still
	// present); display-mode: standalone is the modern standard (Safari 16.4+).
	const isStandalone =
		window.matchMedia(MEDIA_QUERIES.DISPLAY_MODE_STANDALONE).matches ||
		(navigator as Navigator & { standalone?: boolean }).standalone === true;

	return { isIOSDevice, isIOSSafari, isWKWebView, isStandalone };
}

export const device = $state<DeviceContext>(detect());

if (browser) {
	// isStandalone can change at runtime (e.g. user installs the PWA while the
	// tab is open); the UA-derived flags are static for the session.
	const mql = window.matchMedia(MEDIA_QUERIES.DISPLAY_MODE_STANDALONE);

	mql.addEventListener('change', (e) => {
		device.isStandalone = e.matches;
	});
}