Skip to content

Instantly share code, notes, and snippets.

@thijssmudde
Last active June 23, 2025 23:04
Show Gist options
  • Select an option

  • Save thijssmudde/a6c439462e8341624e9c52a54158ea5c to your computer and use it in GitHub Desktop.

Select an option

Save thijssmudde/a6c439462e8341624e9c52a54158ea5c to your computer and use it in GitHub Desktop.
import {
type ReactNode,
createContext,
useContext,
useMemo,
useState,
useLayoutEffect,
useEffect,
} from "react";
import { useIsIOS } from "@/hooks/useIsIOS";
import { useIsAndroid } from "@/hooks/useIsAndroid";
const MOBILE_BREAKPOINT = 668;
interface MobileContextType {
isMobile: boolean;
isIOS: boolean | undefined;
isAndroid: boolean | undefined;
isPWA: boolean | undefined;
isMobileScreen: boolean;
isTouch: boolean;
}
const MobileContext = createContext<MobileContextType | undefined>(undefined);
export function useMobile() {
const context = useContext(MobileContext);
if (context === undefined) {
throw new Error("useMobile must be used within a MobileProvider");
}
return context;
}
export function MobileProvider({ children }: { children: ReactNode }) {
const [isMobileScreen, setIsMobileScreen] = useState(false);
const [isTouch, setIsTouch] = useState(false);
const isIOS = useIsIOS();
const isAndroid = useIsAndroid();
const isPWA = isIOS || isAndroid;
// Safely handle useLayoutEffect during SSR
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
// Detect touch capability
useEffect(() => {
const detectTouch = () => {
return (
typeof window !== "undefined" &&
("ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
(navigator as any).msMaxTouchPoints > 0)
);
};
setIsTouch(detectTouch());
}, []);
useIsomorphicLayoutEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobileScreen(mql.matches);
};
// Set initial value
setIsMobileScreen(mql.matches);
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, []);
const value = useMemo(
() => ({
isMobile: isMobileScreen,
isPWA,
isIOS,
isAndroid,
isMobileScreen,
isTouch,
}),
[isMobileScreen, isTouch, isIOS, isAndroid, isPWA],
);
return <MobileContext.Provider value={value}>{children}</MobileContext.Provider>;
}
import { useEffect, useState } from "react";
export const useIsAndroid = () => {
const [isAndroid, setIsAndroid] = useState<boolean | undefined>(undefined);
useEffect(() => {
const checkIsAndroid = () => {
// Check if we're in a browser environment
if (typeof window === "undefined") {
return false;
}
// Check for Android PWA installation indicators
const userAgent = navigator.userAgent.toLowerCase();
// Check if running on Android device
const isAndroidDevice = /android/.test(userAgent);
if (!isAndroidDevice) {
return false;
}
// Check if it's a PWA - multiple indicators
const isPWA =
// Check for standalone display mode (PWA installed)
window.matchMedia("(display-mode: standalone)").matches ||
// Check for fullscreen display mode
window.matchMedia("(display-mode: fullscreen)").matches ||
// Check for minimal-ui display mode
window.matchMedia("(display-mode: minimal-ui)").matches ||
// Check if launched from home screen (Android WebAPK)
document.referrer?.includes("android-app://") ||
// Check for Android WebAPK user agent
userAgent.includes("wv") || // WebView
// Check for custom user agent added by Android WebAPK
userAgent.includes("packagename") ||
// Check navigator standalone for older implementations
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
return isAndroidDevice && isPWA;
};
const result = checkIsAndroid();
setIsAndroid(result);
}, []);
return isAndroid;
};
import { useEffect, useState } from "react";
export const useIsIOS = () => {
const [isIOS, setIsIOS] = useState<boolean | undefined>(undefined);
useEffect(() => {
const checkIsIOS = () => {
// Check if we're in a browser environment
if (typeof window === "undefined") {
return false;
}
// Check for PWA installation on iOS
const isPWAShell = navigator.userAgent.includes("PWAShell");
const cookies = document.cookie.split(";");
const appPlatformCookie = cookies.find((cookie) => cookie.trim().startsWith("app-platform="));
const isIOSAppStore = appPlatformCookie?.split("=")[1] === "iOS App Store";
return isPWAShell || isIOSAppStore;
};
const result = checkIsIOS();
setIsIOS(result);
}, []);
return isIOS;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment