Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-bugs-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reown/appkit-controllers': patch
---

fix: improve isMobile() to exclude touchscreen desktops using any-pointer:fine media query
25 changes: 19 additions & 6 deletions packages/controllers/src/utils/CoreHelperUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ export const CoreHelperUtil = {

isMobile() {
if (this.isClient()) {
return Boolean(
(window?.matchMedia &&
typeof window.matchMedia === 'function' &&
window.matchMedia('(pointer:coarse)')?.matches) ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/u.test(navigator.userAgent)
)
// Known mobile UA strings - definitive match, check first
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/u.test(navigator.userAgent)) {
return true
}

/*
* Pointer:coarse matches touchscreens, but also touchscreen desktops/laptops.
* If a fine pointer (trackpad/mouse) is also available via any-pointer:fine,
* this is a multi-input desktop device - not mobile.
* Ref: https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/
*/
const isCoarsePointer = window.matchMedia?.('(pointer:coarse)')?.matches ?? false
const hasAnyFinePointer = window.matchMedia?.('(any-pointer:fine)')?.matches ?? false

if (isCoarsePointer && hasAnyFinePointer) {
return false
}

return isCoarsePointer
}

return false
Expand Down
76 changes: 75 additions & 1 deletion packages/controllers/tests/utils/CoreHelperUtil.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'

import { CoreHelperUtil } from '../../src/utils/CoreHelperUtil.js'

Expand Down Expand Up @@ -124,6 +124,80 @@ describe('CoreHelperUtil', () => {
expect(CoreHelperUtil.isCaipAddress(caipAddress)).toEqual(expected)
})

describe('isMobile', () => {
function mockMatchMedia(results: Record<string, boolean>) {
vi.spyOn(window, 'matchMedia').mockImplementation(
(query: string) =>
({
matches: results[query] ?? false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}) as MediaQueryList
)
}

afterEach(() => {
vi.restoreAllMocks()
})

it('should return true for iPhone user agent', () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
)

expect(CoreHelperUtil.isMobile()).toBe(true)
})

it('should return true for Android user agent', () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
)

expect(CoreHelperUtil.isMobile()).toBe(true)
})

it('should return false for non-touch desktop', () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
)
mockMatchMedia({
'(pointer:coarse)': false,
'(any-pointer:fine)': true
})

expect(CoreHelperUtil.isMobile()).toBe(false)
})

it('should return false for touchscreen desktop/laptop', () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
)
mockMatchMedia({
'(pointer:coarse)': true,
'(any-pointer:fine)': true
})

expect(CoreHelperUtil.isMobile()).toBe(false)
})

it('should return true for coarse-only device without mobile UA', () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
'Mozilla/5.0 (X11; CrOS armv7l 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
)
mockMatchMedia({
'(pointer:coarse)': true,
'(any-pointer:fine)': false
})

expect(CoreHelperUtil.isMobile()).toBe(true)
})
})

describe('formatNativeUrl', () => {
const wcUri = 'wc:1234@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=key'

Expand Down
Loading