Skip to content

Commit 8b0991c

Browse files
Feature/pwa detection (#1460)
* CSP header updates Updated urls for content security policy to be passed * pwa detection * adding debugview to true * pwa or webapp loaded event in GA --------- Co-authored-by: Kylee Fields <43586156+kyleecodes@users.noreply.github.com>
1 parent d2c3c77 commit 8b0991c

File tree

4 files changed

+170
-2
lines changed

4 files changed

+170
-2
lines changed

app/layout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
import { ReactNode } from 'react';
1+
'use client';
2+
3+
import { PWA_LOADED, WEB_APP_LOADED } from '@/lib/constants/events';
4+
import logEvent from '@/lib/utils/logEvent';
5+
import { storePWAStatus, usePWAStatus } from '@/lib/utils/pwaDetection';
6+
import { ReactNode, useEffect } from 'react';
27

38
type Props = {
49
children: ReactNode;
510
};
611

712
// Since we have a `not-found.tsx` page on the root, a layout file is required to pass children
813
export default function RootLayout({ children }: Props) {
14+
const pwaStatus = usePWAStatus();
15+
16+
useEffect(() => {
17+
if (pwaStatus) {
18+
// Store status for future reference
19+
storePWAStatus(pwaStatus);
20+
pwaStatus.isInstalled ? logEvent(PWA_LOADED) : logEvent(WEB_APP_LOADED);
21+
}
22+
}, [pwaStatus]);
923
return children;
1024
}

components/layout/BaseLayout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export default async function BaseLayout({ children, locale }: BaseLayoutProps)
100100
)}
101101
<Analytics />
102102
</body>
103-
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID || ''} />
103+
<GoogleAnalytics
104+
debugMode={true}
105+
gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID || ''}
106+
/>
104107
</StoryblokProvider>
105108
</ThemeProvider>
106109
</AppRouterCacheProvider>

lib/constants/events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,7 @@ export const SIGN_UP_TODAY_BANNER_BUTTON_CLICKED = 'SIGN_UP_TODAY_BANNER_BUTTON_
249249
// PWA EVENTS
250250
export const PWA_INSTALLED = 'PWA_INSTALLED';
251251
export const PWA_DISMISSED = 'PWA_DISMISSED';
252+
253+
// TYPE OF APP LOADED
254+
export const PWA_LOADED = 'PWA_LOADED';
255+
export const WEB_APP_LOADED = 'WEB_APP_LOADED';

lib/utils/pwaDetection.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export interface PWAStatus {
4+
isInstalled: boolean;
5+
installSource: string;
6+
displayMode: string;
7+
detectionMethods: Record<string, boolean>;
8+
}
9+
10+
/**
11+
* Detects if the app is running as an installed PWA
12+
* @returns PWA status information or null if running on server
13+
*/
14+
export const detectPWA = (): PWAStatus | null => {
15+
// Early return if running on server
16+
if (typeof window === 'undefined') {
17+
return null;
18+
}
19+
20+
// Basic display mode checks
21+
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
22+
const isFullScreen = window.matchMedia('(display-mode: fullscreen)').matches;
23+
const isMinimalUi = window.matchMedia('(display-mode: minimal-ui)').matches;
24+
const isWindowControls = window.matchMedia('(display-mode: window-controls-overlay)').matches;
25+
26+
// Platform-specific checks
27+
const iosPWA = Boolean((window.navigator as any).standalone);
28+
const isWindowsApp = navigator.userAgent.includes('MSAppHost');
29+
const isAndroidPWA = document.referrer.includes('android-app://');
30+
31+
// Technical checks
32+
const hasServiceWorker = 'serviceWorker' in navigator;
33+
const hasManifest = !!Array.from(document.querySelectorAll('link')).find(
34+
(link) => link.rel === 'manifest',
35+
);
36+
const isHttps = window.location.protocol === 'https:';
37+
38+
// Combined determination
39+
const detectionMethods = {
40+
isStandalone,
41+
isFullScreen,
42+
isMinimalUi,
43+
isWindowControls,
44+
iosPWA,
45+
isWindowsApp,
46+
isAndroidPWA,
47+
hasServiceWorker,
48+
hasManifest,
49+
isHttps,
50+
};
51+
52+
const isInstalled =
53+
isStandalone ||
54+
isFullScreen ||
55+
isMinimalUi ||
56+
isWindowControls ||
57+
iosPWA ||
58+
isWindowsApp ||
59+
isAndroidPWA;
60+
61+
// Determine display mode
62+
let displayMode = 'browser';
63+
if (isStandalone) displayMode = 'standalone';
64+
if (isFullScreen) displayMode = 'fullscreen';
65+
if (isMinimalUi) displayMode = 'minimal-ui';
66+
if (isWindowControls) displayMode = 'window-controls-overlay';
67+
68+
// Determine install source
69+
let installSource = 'browser';
70+
if (isAndroidPWA) {
71+
installSource = 'Android';
72+
} else if (iosPWA) {
73+
installSource = 'iOS';
74+
} else if (isWindowsApp) {
75+
installSource = 'Windows Store';
76+
} else if (isWindowControls) {
77+
installSource = 'Chrome Desktop';
78+
} else if (isStandalone || isFullScreen || isMinimalUi) {
79+
installSource = 'Home Screen';
80+
}
81+
82+
return {
83+
isInstalled,
84+
installSource,
85+
displayMode,
86+
detectionMethods,
87+
};
88+
};
89+
90+
/**
91+
* Hook to monitor PWA installation status
92+
*/
93+
export const usePWAStatus = () => {
94+
const [pwaStatus, setPwaStatus] = useState<PWAStatus | null>(null);
95+
96+
useEffect(() => {
97+
// Initial check
98+
setPwaStatus(detectPWA());
99+
100+
// Set up listeners for display mode changes
101+
const modeQueries = [
102+
window.matchMedia('(display-mode: standalone)'),
103+
window.matchMedia('(display-mode: fullscreen)'),
104+
window.matchMedia('(display-mode: minimal-ui)'),
105+
window.matchMedia('(display-mode: window-controls-overlay)'),
106+
];
107+
108+
const handleDisplayModeChange = () => {
109+
setPwaStatus(detectPWA());
110+
};
111+
112+
modeQueries.forEach((query) => {
113+
query.addEventListener('change', handleDisplayModeChange);
114+
});
115+
116+
return () => {
117+
modeQueries.forEach((query) => {
118+
query.removeEventListener('change', handleDisplayModeChange);
119+
});
120+
};
121+
}, []);
122+
123+
return pwaStatus;
124+
};
125+
126+
/**
127+
* Store PWA status in local storage
128+
*/
129+
export const storePWAStatus = (status: PWAStatus | null): void => {
130+
if (typeof localStorage === 'undefined' || !status) return;
131+
132+
localStorage.setItem('pwaInstalled', String(status.isInstalled));
133+
localStorage.setItem('pwaInstallSource', status.installSource);
134+
localStorage.setItem('pwaDisplayMode', status.displayMode);
135+
};
136+
137+
/**
138+
* Get previously stored PWA status
139+
*/
140+
export const getStoredPWAStatus = (): { isInstalled: boolean; source: string } | null => {
141+
if (typeof localStorage === 'undefined') return null;
142+
143+
const isInstalled = localStorage.getItem('pwaInstalled') === 'true';
144+
const source = localStorage.getItem('pwaInstallSource') || '';
145+
146+
return { isInstalled, source };
147+
};

0 commit comments

Comments
 (0)