Last active
June 23, 2025 23:04
-
-
Save thijssmudde/a6c439462e8341624e9c52a54158ea5c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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