Fix mobile payment launch detection

This commit is contained in:
IanShaw027
2026-04-21 09:22:40 -07:00
parent da1d26001f
commit 906802abe3
5 changed files with 164 additions and 12 deletions

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { detectMobileDevice } from '../device'
describe('detectMobileDevice', () => {
it('prefers userAgentData.mobile when available', () => {
expect(detectMobileDevice({
navigator: {
userAgent: 'Mozilla/5.0',
userAgentData: { mobile: true },
},
})).toBe(true)
})
it('recognizes handheld browsers from the mobile UA token', () => {
expect(detectMobileDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/136.0 Mobile Safari/537.36',
maxTouchPoints: 5,
},
})).toBe(true)
})
it('recognizes iPadOS desktop mode via touch capability', () => {
expect(detectMobileDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15',
platform: 'MacIntel',
maxTouchPoints: 5,
},
})).toBe(true)
})
it('falls back to input capability detection for touch-first devices', () => {
expect(detectMobileDevice({
navigator: {
userAgent: 'Mozilla/5.0',
maxTouchPoints: 10,
},
matchMedia: (query) => ({
matches: query === '(pointer: coarse)' || query === '(hover: none)',
}),
})).toBe(true)
})
it('keeps desktop environments as non-mobile', () => {
expect(detectMobileDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/136.0 Safari/537.36',
platform: 'MacIntel',
maxTouchPoints: 0,
},
matchMedia: () => ({ matches: false }),
})).toBe(false)
})
})

View File

@@ -1,11 +1,62 @@
/**
* Detect whether the current device is mobile.
* Uses navigator.userAgentData (modern API) with UA regex fallback.
*/
export function isMobileDevice(): boolean {
const nav = navigator as unknown as Record<string, unknown>
if (nav.userAgentData && typeof (nav.userAgentData as Record<string, unknown>).mobile === 'boolean') {
return (nav.userAgentData as Record<string, unknown>).mobile as boolean
}
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent)
interface NavigatorUADataLike {
mobile?: boolean
}
interface NavigatorLike {
userAgent?: string
platform?: string
maxTouchPoints?: number
userAgentData?: NavigatorUADataLike
}
interface MediaQueryResultLike {
matches: boolean
}
interface DeviceDetectionEnvironment {
navigator?: NavigatorLike
matchMedia?: (query: string) => MediaQueryResultLike | null | undefined
}
const MOBILE_UA_RE = /\b(Mobi|Android|iPhone|iPod|Windows Phone|webOS|BlackBerry|IEMobile)\b/i
const TABLET_UA_RE = /\b(iPad|Tablet)\b/i
function matchesQuery(
matchMedia: DeviceDetectionEnvironment['matchMedia'],
query: string,
): boolean {
try {
return matchMedia?.(query)?.matches === true
} catch {
return false
}
}
export function detectMobileDevice(env: DeviceDetectionEnvironment = {}): boolean {
const nav = env.navigator
if (!nav) return false
if (nav.userAgentData?.mobile === true) {
return true
}
const userAgent = nav.userAgent || ''
const maxTouchPoints = nav.maxTouchPoints ?? 0
const isIPadOSDesktopMode = nav.platform === 'MacIntel' && maxTouchPoints > 1
const isMobileUA = MOBILE_UA_RE.test(userAgent)
const isTabletUA = TABLET_UA_RE.test(userAgent) || isIPadOSDesktopMode
const coarsePointer = matchesQuery(env.matchMedia, '(pointer: coarse)')
const noHover = matchesQuery(env.matchMedia, '(hover: none)')
const hasTouch = maxTouchPoints > 0
return isMobileUA || isTabletUA || (coarsePointer && noHover && hasTouch)
}
export function isMobileDevice(): boolean {
if (typeof navigator === 'undefined') return false
return detectMobileDevice({
navigator,
matchMedia: typeof window !== 'undefined' ? window.matchMedia.bind(window) : undefined,
})
}