diff --git a/.changeset/platform-user-agent-data.md b/.changeset/platform-user-agent-data.md
new file mode 100644
index 00000000..01b8b4e1
--- /dev/null
+++ b/.changeset/platform-user-agent-data.md
@@ -0,0 +1,5 @@
+---
+'react-simplikit': patch
+---
+
+Prefer `navigator.userAgentData` when detecting iOS and Android platforms
diff --git a/packages/plugin/skills/react-simplikit/references/isAndroid.md b/packages/plugin/skills/react-simplikit/references/isAndroid.md
index a6504105..c076a0ea 100644
--- a/packages/plugin/skills/react-simplikit/references/isAndroid.md
+++ b/packages/plugin/skills/react-simplikit/references/isAndroid.md
@@ -2,9 +2,13 @@
`isAndroid` is a utility function that detects whether the current device is running Android.
+Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+and falls back to matching `"Android"` in Client Hints brands or `navigator.userAgent`.
+
Notes:
- All Android browsers include the token "Android" in the user agent.
+- Chromium on Android typically exposes Client Hints `platform` as `"Android"`.
## Interface
diff --git a/packages/plugin/skills/react-simplikit/references/isIOS.md b/packages/plugin/skills/react-simplikit/references/isIOS.md
index 71bc4d7f..12186ced 100644
--- a/packages/plugin/skills/react-simplikit/references/isIOS.md
+++ b/packages/plugin/skills/react-simplikit/references/isIOS.md
@@ -2,12 +2,16 @@
`isIOS` is a utility function that detects whether the current device is running iOS or iPadOS.
+Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+and falls back to the legacy `navigator.platform` / `navigator.userAgent` strings.
+
Notes on platform inconsistencies:
- Prior to iPadOS 13, iPads reported their platform as "iPad" (or matched /iPad/ in UA).
- Starting from iPadOS 13, Apple changed the platform string to "MacIntel"
to make websites treat iPadOS as desktop-class Safari.
However, these devices still expose multi-touch capabilities.
+- Chromium-based browsers may report Client Hints `platform` as `"iOS"` instead of `"iPhone"`.
## Interface
@@ -20,7 +24,7 @@ function isIOS(userAgent?: string): boolean;
### Return Value
diff --git a/packages/react-simplikit/src/_internal/platform/testPlatform.test.ts b/packages/react-simplikit/src/_internal/platform/testPlatform.test.ts
new file mode 100644
index 00000000..d94042fc
--- /dev/null
+++ b/packages/react-simplikit/src/_internal/platform/testPlatform.test.ts
@@ -0,0 +1,88 @@
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { serverEnvironments } from '../test-utils/serverEnvironments.ts';
+
+import { testPlatform } from './testPlatform.ts';
+import type { NavigatorUAData } from './types.ts';
+
+describe('testPlatform', () => {
+ const originalPlatform = navigator.platform;
+ const originalDescriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData');
+
+ afterEach(function restoreNavigator() {
+ Object.defineProperty(navigator, 'platform', {
+ value: originalPlatform,
+ configurable: true,
+ });
+
+ if (originalDescriptor == null) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- cleanup test stub
+ delete (navigator as any).userAgentData;
+ return;
+ }
+
+ Object.defineProperty(Navigator.prototype, 'userAgentData', originalDescriptor);
+ });
+
+ function stubUserAgentData(userAgentData: NavigatorUAData | undefined) {
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: userAgentData,
+ configurable: true,
+ });
+ }
+
+ it('prefers userAgentData.platform over navigator.platform', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'Android',
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: 'Linux armv8l',
+ configurable: true,
+ });
+
+ expect(testPlatform(/^Android/i)).toBe(true);
+ expect(testPlatform(/^Linux/i)).toBe(false);
+ });
+
+ it('falls back to navigator.platform when userAgentData is missing', () => {
+ stubUserAgentData(undefined);
+ Object.defineProperty(navigator, 'platform', {
+ value: 'iPhone',
+ configurable: true,
+ });
+
+ expect(testPlatform(/^iPhone/i)).toBe(true);
+ });
+
+ it('matches iOS Client Hints platform', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'iOS',
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: 'iPhone',
+ configurable: true,
+ });
+
+ expect(testPlatform(/^(iPhone|iPad|iPod|iOS)/i)).toBe(true);
+ });
+
+ it('returns false when the platform does not match', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: false,
+ platform: 'Windows',
+ });
+
+ expect(testPlatform(/^Android/i)).toBe(false);
+ });
+
+ it('returns false on the server', () => {
+ const result = serverEnvironments(() => testPlatform(/^Android/i));
+
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/react-simplikit/src/_internal/platform/testPlatform.ts b/packages/react-simplikit/src/_internal/platform/testPlatform.ts
new file mode 100644
index 00000000..a8419129
--- /dev/null
+++ b/packages/react-simplikit/src/_internal/platform/testPlatform.ts
@@ -0,0 +1,16 @@
+import type { NavigatorWithUserAgentData } from './types.ts';
+
+/**
+ * Tests a regular expression against `navigator.userAgentData.platform` when
+ * available, otherwise against the legacy `navigator.platform` string.
+ */
+export function testPlatform(re: RegExp): boolean {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ const navigatorWithHints = window.navigator as NavigatorWithUserAgentData;
+ const platform = navigatorWithHints.userAgentData?.platform ?? window.navigator.platform;
+
+ return re.test(platform);
+}
diff --git a/packages/react-simplikit/src/_internal/platform/testUserAgent.test.ts b/packages/react-simplikit/src/_internal/platform/testUserAgent.test.ts
new file mode 100644
index 00000000..afd5b1ee
--- /dev/null
+++ b/packages/react-simplikit/src/_internal/platform/testUserAgent.test.ts
@@ -0,0 +1,94 @@
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { serverEnvironments } from '../test-utils/serverEnvironments.ts';
+
+import { testUserAgent } from './testUserAgent.ts';
+import type { NavigatorUAData } from './types.ts';
+
+describe('testUserAgent', () => {
+ const originalUserAgent = navigator.userAgent;
+ const originalDescriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData');
+
+ afterEach(function restoreNavigator() {
+ Object.defineProperty(navigator, 'userAgent', {
+ value: originalUserAgent,
+ configurable: true,
+ });
+
+ if (originalDescriptor == null) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- cleanup test stub
+ delete (navigator as any).userAgentData;
+ return;
+ }
+
+ Object.defineProperty(Navigator.prototype, 'userAgentData', originalDescriptor);
+ });
+
+ function stubUserAgentData(userAgentData: NavigatorUAData | undefined) {
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: userAgentData,
+ configurable: true,
+ });
+ }
+
+ it('returns true when a Client Hints brand matches', () => {
+ stubUserAgentData({
+ brands: [
+ { brand: 'Chromium', version: '120' },
+ { brand: 'Google Chrome', version: '120' },
+ ],
+ mobile: false,
+ platform: 'Windows',
+ });
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
+ configurable: true,
+ });
+
+ expect(testUserAgent(/Chrome/i)).toBe(true);
+ });
+
+ it('falls back to navigator.userAgent when brands do not match', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'Android',
+ });
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Chrome/126',
+ configurable: true,
+ });
+
+ expect(testUserAgent(/Android/i)).toBe(true);
+ });
+
+ it('falls back to navigator.userAgent when userAgentData is missing', () => {
+ stubUserAgentData(undefined);
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Chrome/126',
+ configurable: true,
+ });
+
+ expect(testUserAgent(/Android/i)).toBe(true);
+ });
+
+ it('returns false when neither brands nor userAgent match', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: false,
+ platform: 'Windows',
+ });
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
+ configurable: true,
+ });
+
+ expect(testUserAgent(/Android/i)).toBe(false);
+ });
+
+ it('returns false on the server', () => {
+ const result = serverEnvironments(() => testUserAgent(/Android/i));
+
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/react-simplikit/src/_internal/platform/testUserAgent.ts b/packages/react-simplikit/src/_internal/platform/testUserAgent.ts
new file mode 100644
index 00000000..40e20e5b
--- /dev/null
+++ b/packages/react-simplikit/src/_internal/platform/testUserAgent.ts
@@ -0,0 +1,23 @@
+import type { NavigatorWithUserAgentData } from './types.ts';
+
+/**
+ * Tests a regular expression against Client Hints brand list when available,
+ * otherwise against `navigator.userAgent`.
+ *
+ * Brand matching is useful for browser engines (Chrome, Firefox, …).
+ * Platform tokens such as "Android" usually appear only in the UA string.
+ */
+export function testUserAgent(re: RegExp): boolean {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ const navigatorWithHints = window.navigator as NavigatorWithUserAgentData;
+ const brands = navigatorWithHints.userAgentData?.brands;
+
+ if (Array.isArray(brands) && brands.some(brand => re.test(brand.brand))) {
+ return true;
+ }
+
+ return re.test(window.navigator.userAgent);
+}
diff --git a/packages/react-simplikit/src/_internal/platform/types.ts b/packages/react-simplikit/src/_internal/platform/types.ts
new file mode 100644
index 00000000..6500bcfd
--- /dev/null
+++ b/packages/react-simplikit/src/_internal/platform/types.ts
@@ -0,0 +1,19 @@
+/**
+ * Minimal typings for the User-Agent Client Hints API.
+ * TypeScript's bundled DOM lib does not include these yet in this project.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
+ */
+export type NavigatorUABrandVersion = {
+ brand: string;
+ version: string;
+};
+
+export type NavigatorUAData = {
+ brands: NavigatorUABrandVersion[];
+ mobile: boolean;
+ platform: string;
+};
+
+export type NavigatorWithUserAgentData = Navigator & {
+ userAgentData?: NavigatorUAData;
+};
diff --git a/packages/react-simplikit/src/utils/isAndroid/es/isAndroid.md b/packages/react-simplikit/src/utils/isAndroid/es/isAndroid.md
index 250f7cbb..803b1cb0 100644
--- a/packages/react-simplikit/src/utils/isAndroid/es/isAndroid.md
+++ b/packages/react-simplikit/src/utils/isAndroid/es/isAndroid.md
@@ -2,9 +2,12 @@
`isAndroid` es una función de utilidad que detecta si el dispositivo actual usa Android.
+Prioriza la API User-Agent Client Hints (`navigator.userAgentData.platform`) y, si no está disponible, busca `"Android"` en las marcas de Client Hints o en `navigator.userAgent`.
+
Notas:
- Todos los navegadores de Android incluyen el token “Android” en el agente de usuario.
+- Chromium en Android suele exponer el `platform` de Client Hints como `"Android"`.
## Interfaz
@@ -17,7 +20,7 @@ function isAndroid(userAgent?: string): boolean;
### Valor de retorno
@@ -25,7 +28,7 @@ function isAndroid(userAgent?: string): boolean;
## Ejemplo
diff --git a/packages/react-simplikit/src/utils/isAndroid/isAndroid.md b/packages/react-simplikit/src/utils/isAndroid/isAndroid.md
index a6504105..c076a0ea 100644
--- a/packages/react-simplikit/src/utils/isAndroid/isAndroid.md
+++ b/packages/react-simplikit/src/utils/isAndroid/isAndroid.md
@@ -2,9 +2,13 @@
`isAndroid` is a utility function that detects whether the current device is running Android.
+Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+and falls back to matching `"Android"` in Client Hints brands or `navigator.userAgent`.
+
Notes:
- All Android browsers include the token "Android" in the user agent.
+- Chromium on Android typically exposes Client Hints `platform` as `"Android"`.
## Interface
diff --git a/packages/react-simplikit/src/utils/isAndroid/isAndroid.test.ts b/packages/react-simplikit/src/utils/isAndroid/isAndroid.test.ts
index bf1c2364..b73cbd5d 100644
--- a/packages/react-simplikit/src/utils/isAndroid/isAndroid.test.ts
+++ b/packages/react-simplikit/src/utils/isAndroid/isAndroid.test.ts
@@ -1,8 +1,52 @@
-import { describe, expect, it } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import type { NavigatorUAData } from '../../_internal/platform/types.ts';
import { isAndroid } from './isAndroid.ts';
describe('isAndroid', () => {
+ const originalUserAgent = navigator.userAgent;
+ const originalPlatform = navigator.platform;
+ const originalUserAgentDataDescriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData');
+
+ function stubUserAgentData(userAgentData: NavigatorUAData | undefined) {
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: userAgentData,
+ configurable: true,
+ });
+ }
+
+ beforeEach(() => {
+ Object.defineProperty(navigator, 'userAgent', {
+ value: originalUserAgent,
+ configurable: true,
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: originalPlatform,
+ configurable: true,
+ });
+ stubUserAgentData(undefined);
+ });
+
+ afterEach(() => {
+ Object.defineProperty(navigator, 'userAgent', {
+ value: originalUserAgent,
+ configurable: true,
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: originalPlatform,
+ configurable: true,
+ });
+
+ if (originalUserAgentDataDescriptor == null) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- cleanup test stub
+ delete (navigator as any).userAgentData;
+ return;
+ }
+
+ Object.defineProperty(Navigator.prototype, 'userAgentData', originalUserAgentDataDescriptor);
+ });
+
it('should detect Android (Pixel)', () => {
expect(isAndroid('Mozilla/5.0 (Linux; Android 12; Pixel 6) Chrome/120')).toBe(true);
});
@@ -24,17 +68,43 @@ describe('isAndroid', () => {
});
it('should fall back to navigator.userAgent when no argument is given', () => {
- const originalUserAgent = navigator.userAgent;
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Chrome/126',
configurable: true,
});
expect(isAndroid()).toBe(true);
+ });
+ it('should detect Android from userAgentData.platform', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'Android',
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: 'Linux armv8l',
+ configurable: true,
+ });
Object.defineProperty(navigator, 'userAgent', {
- value: originalUserAgent,
+ value: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120',
configurable: true,
});
+
+ expect(isAndroid()).toBe(true);
+ });
+
+ it('should return false when Client Hints and userAgent are not Android', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: false,
+ platform: 'Windows',
+ });
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120',
+ configurable: true,
+ });
+
+ expect(isAndroid()).toBe(false);
});
});
diff --git a/packages/react-simplikit/src/utils/isAndroid/isAndroid.ts b/packages/react-simplikit/src/utils/isAndroid/isAndroid.ts
index 3ac5e64c..51690d5b 100644
--- a/packages/react-simplikit/src/utils/isAndroid/isAndroid.ts
+++ b/packages/react-simplikit/src/utils/isAndroid/isAndroid.ts
@@ -1,11 +1,17 @@
+import { testPlatform } from '../../_internal/platform/testPlatform.ts';
+import { testUserAgent } from '../../_internal/platform/testUserAgent.ts';
import { isServer } from '../isServer/index.ts';
/**
* @description
* `isAndroid` is a utility function that detects whether the current device is running Android.
*
+ * Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+ * and falls back to matching `"Android"` in Client Hints brands or `navigator.userAgent`.
+ *
* Notes:
* - All Android browsers include the token "Android" in the user agent.
+ * - Chromium on Android typically exposes Client Hints `platform` as `"Android"`.
*
* @param {string} [userAgent] - Optional user agent string to check. Defaults to `navigator.userAgent`.
*
@@ -27,6 +33,9 @@ export function isAndroid(userAgent?: string): boolean {
return false;
}
- const ua = userAgent ?? navigator.userAgent;
- return /Android/i.test(ua);
+ if (userAgent !== undefined) {
+ return /Android/i.test(userAgent);
+ }
+
+ return testPlatform(/^Android/i) || testUserAgent(/Android/i);
}
diff --git a/packages/react-simplikit/src/utils/isAndroid/ja/isAndroid.md b/packages/react-simplikit/src/utils/isAndroid/ja/isAndroid.md
index bbd8c962..7a187551 100644
--- a/packages/react-simplikit/src/utils/isAndroid/ja/isAndroid.md
+++ b/packages/react-simplikit/src/utils/isAndroid/ja/isAndroid.md
@@ -2,9 +2,12 @@
`isAndroid` は、現在のデバイスで Android が動作しているかどうかを判定するユーティリティ関数です。
+User-Agent Client Hints API(`navigator.userAgentData.platform`)を優先し、利用できない場合は Client Hints の brands または `navigator.userAgent` で `"Android"` を照合します。
+
補足:
- すべての Android ブラウザーのユーザーエージェントには、トークン「Android」が含まれます。
+- Android 上の Chromium は、通常 Client Hints の `platform` を `"Android"` として公開します。
## インターフェース
@@ -17,7 +20,7 @@ function isAndroid(userAgent?: string): boolean;
### 戻り値
@@ -25,20 +28,20 @@ function isAndroid(userAgent?: string): boolean;
-## 使用例
+## 例
```tsx
if (isAndroid()) {
- // Android 固有のコード
+ // Android 固有の処理
enableAndroidOptimizations();
}
```
```tsx
-// カスタムのユーザーエージェントを指定する場合
+// ユーザーエージェントを直接渡す場合
const isAndroidDevice = isAndroid(
'Mozilla/5.0 (Linux; Android 12; Pixel 6) Chrome/120'
);
diff --git a/packages/react-simplikit/src/utils/isAndroid/ko/isAndroid.md b/packages/react-simplikit/src/utils/isAndroid/ko/isAndroid.md
index 7a8e0820..2dd33ea3 100644
--- a/packages/react-simplikit/src/utils/isAndroid/ko/isAndroid.md
+++ b/packages/react-simplikit/src/utils/isAndroid/ko/isAndroid.md
@@ -2,9 +2,12 @@
`isAndroid`는 현재 기기가 안드로이드에서 실행 중인지 감지하는 유틸리티 함수예요.
+User-Agent Client Hints API(`navigator.userAgentData.platform`)를 우선 사용하고, 없으면 Client Hints brands 또는 `navigator.userAgent`에서 `"Android"`를 매칭해요.
+
**참고 사항**
- 모든 안드로이드 브라우저는 사용자 에이전트에 'Android' 토큰을 포함해요.
+- 안드로이드의 Chromium은 Client Hints `platform`을 보통 `"Android"`로 노출해요.
## 인터페이스
diff --git a/packages/react-simplikit/src/utils/isAndroid/zh-Hans/isAndroid.md b/packages/react-simplikit/src/utils/isAndroid/zh-Hans/isAndroid.md
index e4ae8040..54711e4f 100644
--- a/packages/react-simplikit/src/utils/isAndroid/zh-Hans/isAndroid.md
+++ b/packages/react-simplikit/src/utils/isAndroid/zh-Hans/isAndroid.md
@@ -2,9 +2,12 @@
`isAndroid` 是一个检测当前设备是否运行 Android 的工具函数。
+优先使用 User-Agent Client Hints API(`navigator.userAgentData.platform`),不可用时回退到在 Client Hints brands 或 `navigator.userAgent` 中匹配 `"Android"`。
+
注意:
- 所有 Android 浏览器的 user agent 中都包含“Android”字符串。
+- Android 上的 Chromium 通常将 Client Hints 的 `platform` 暴露为 `"Android"`。
## 接口
@@ -17,7 +20,7 @@ function isAndroid(userAgent?: string): boolean;
### 返回值
@@ -25,20 +28,20 @@ function isAndroid(userAgent?: string): boolean;
## 示例
```tsx
if (isAndroid()) {
- // Android-specific code
+ // Android 专用代码
enableAndroidOptimizations();
}
```
```tsx
-// With custom user agent
+// 传入自定义 user agent
const isAndroidDevice = isAndroid(
'Mozilla/5.0 (Linux; Android 12; Pixel 6) Chrome/120'
);
diff --git a/packages/react-simplikit/src/utils/isIOS/es/isIOS.md b/packages/react-simplikit/src/utils/isIOS/es/isIOS.md
index 123b765c..7eb6a387 100644
--- a/packages/react-simplikit/src/utils/isIOS/es/isIOS.md
+++ b/packages/react-simplikit/src/utils/isIOS/es/isIOS.md
@@ -2,12 +2,15 @@
`isIOS` es una función de utilidad que detecta si el dispositivo actual ejecuta iOS o iPadOS.
+Prioriza la API User-Agent Client Hints (`navigator.userAgentData.platform`) y, si no está disponible, recurre a las cadenas heredadas `navigator.platform` / `navigator.userAgent`.
+
Notas sobre las inconsistencias de la plataforma:
- Antes de iPadOS 13, los iPad indicaban “iPad” como su plataforma (o coincidían con /iPad/ en el agente de usuario).
- A partir de iPadOS 13, Apple cambió la cadena de plataforma a “MacIntel”
para que los sitios web trataran iPadOS como Safari de escritorio.
Sin embargo, estos dispositivos siguen exponiendo capacidades multitáctiles.
+- Los navegadores basados en Chromium pueden informar el `platform` de Client Hints como `"iOS"` en lugar de `"iPhone"`.
## Interfaz
@@ -20,7 +23,7 @@ function isIOS(userAgent?: string): boolean;
### Valor de retorno
@@ -28,7 +31,7 @@ function isIOS(userAgent?: string): boolean;
## Ejemplo
diff --git a/packages/react-simplikit/src/utils/isIOS/isIOS.md b/packages/react-simplikit/src/utils/isIOS/isIOS.md
index 71bc4d7f..12186ced 100644
--- a/packages/react-simplikit/src/utils/isIOS/isIOS.md
+++ b/packages/react-simplikit/src/utils/isIOS/isIOS.md
@@ -2,12 +2,16 @@
`isIOS` is a utility function that detects whether the current device is running iOS or iPadOS.
+Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+and falls back to the legacy `navigator.platform` / `navigator.userAgent` strings.
+
Notes on platform inconsistencies:
- Prior to iPadOS 13, iPads reported their platform as "iPad" (or matched /iPad/ in UA).
- Starting from iPadOS 13, Apple changed the platform string to "MacIntel"
to make websites treat iPadOS as desktop-class Safari.
However, these devices still expose multi-touch capabilities.
+- Chromium-based browsers may report Client Hints `platform` as `"iOS"` instead of `"iPhone"`.
## Interface
@@ -20,7 +24,7 @@ function isIOS(userAgent?: string): boolean;
### Return Value
diff --git a/packages/react-simplikit/src/utils/isIOS/isIOS.test.ts b/packages/react-simplikit/src/utils/isIOS/isIOS.test.ts
index 934ee1da..d95d92f2 100644
--- a/packages/react-simplikit/src/utils/isIOS/isIOS.test.ts
+++ b/packages/react-simplikit/src/utils/isIOS/isIOS.test.ts
@@ -1,10 +1,21 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import type { NavigatorUAData } from '../../_internal/platform/types.ts';
+
import { isIOS } from './isIOS.ts';
describe('isIOS', () => {
const originalPlatform = navigator.platform;
const originalMaxTouchPoints = navigator.maxTouchPoints;
+ const originalUserAgent = navigator.userAgent;
+ const originalUserAgentDataDescriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData');
+
+ function stubUserAgentData(userAgentData: NavigatorUAData | undefined) {
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: userAgentData,
+ configurable: true,
+ });
+ }
beforeEach(() => {
// Reset to default values before each test
@@ -16,6 +27,11 @@ describe('isIOS', () => {
value: originalMaxTouchPoints,
configurable: true,
});
+ Object.defineProperty(navigator, 'userAgent', {
+ value: originalUserAgent,
+ configurable: true,
+ });
+ stubUserAgentData(undefined);
});
afterEach(() => {
@@ -28,6 +44,18 @@ describe('isIOS', () => {
value: originalMaxTouchPoints,
configurable: true,
});
+ Object.defineProperty(navigator, 'userAgent', {
+ value: originalUserAgent,
+ configurable: true,
+ });
+
+ if (originalUserAgentDataDescriptor == null) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- cleanup test stub
+ delete (navigator as any).userAgentData;
+ return;
+ }
+
+ Object.defineProperty(Navigator.prototype, 'userAgentData', originalUserAgentDataDescriptor);
});
it('should detect iOS (iPhone)', () => {
@@ -82,17 +110,55 @@ describe('isIOS', () => {
});
it('should fall back to navigator.userAgent when no argument is given', () => {
- const originalUserAgent = navigator.userAgent;
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)',
configurable: true,
});
expect(isIOS()).toBe(true);
+ });
+ it('should detect iOS from userAgentData.platform', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'iOS',
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: 'Win32',
+ configurable: true,
+ });
Object.defineProperty(navigator, 'userAgent', {
- value: originalUserAgent,
+ value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120',
configurable: true,
});
+ Object.defineProperty(navigator, 'maxTouchPoints', {
+ value: 0,
+ configurable: true,
+ });
+
+ expect(isIOS()).toBe(true);
+ });
+
+ it('should detect iPadOS from userAgentData macOS platform with touch', () => {
+ stubUserAgentData({
+ brands: [{ brand: 'Chromium', version: '120' }],
+ mobile: true,
+ platform: 'macOS',
+ });
+ Object.defineProperty(navigator, 'platform', {
+ value: 'MacIntel',
+ configurable: true,
+ });
+ Object.defineProperty(navigator, 'maxTouchPoints', {
+ value: 5,
+ configurable: true,
+ });
+ Object.defineProperty(navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X)',
+ configurable: true,
+ });
+
+ expect(isIOS()).toBe(true);
});
});
diff --git a/packages/react-simplikit/src/utils/isIOS/isIOS.ts b/packages/react-simplikit/src/utils/isIOS/isIOS.ts
index 7866ec50..8cf90230 100644
--- a/packages/react-simplikit/src/utils/isIOS/isIOS.ts
+++ b/packages/react-simplikit/src/utils/isIOS/isIOS.ts
@@ -1,16 +1,24 @@
+import { testPlatform } from '../../_internal/platform/testPlatform.ts';
+import { testUserAgent } from '../../_internal/platform/testUserAgent.ts';
import { isServer } from '../isServer/index.ts';
/**
* @description
* `isIOS` is a utility function that detects whether the current device is running iOS or iPadOS.
*
+ * Detection prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)
+ * and falls back to the legacy `navigator.platform` / `navigator.userAgent` strings.
+ *
* Notes on platform inconsistencies:
* - Prior to iPadOS 13, iPads reported their platform as "iPad" (or matched /iPad/ in UA).
* - Starting from iPadOS 13, Apple changed the platform string to "MacIntel"
* to make websites treat iPadOS as desktop-class Safari.
* However, these devices still expose multi-touch capabilities.
+ * - Chromium-based browsers may report Client Hints `platform` as `"iOS"` instead of `"iPhone"`.
*
* @param {string} [userAgent] - Optional user agent string to check. Defaults to `navigator.userAgent`.
+ * When provided, the string is matched directly; the modern iPadOS heuristic still uses the live
+ * platform and touch APIs so desktop-class iPad Safari can be detected.
*
* @returns {boolean} `true` if the device is running iOS or iPadOS, `false` otherwise.
* Returns `false` on server-side rendering environments.
@@ -30,12 +38,16 @@ export function isIOS(userAgent?: string): boolean {
return false;
}
- const ua = userAgent ?? navigator.userAgent;
- const platform = navigator.platform;
- const maxTouchPoints = navigator.maxTouchPoints;
+ const matchesModernIPad =
+ testPlatform(/^Mac/i) && typeof navigator.maxTouchPoints === 'number' && navigator.maxTouchPoints > 1;
+
+ if (userAgent !== undefined) {
+ const matchesClassicIOS = /iPhone|iPad|iPod/i.test(userAgent);
+ return matchesClassicIOS || matchesModernIPad;
+ }
- const matchesClassicIOS = /iPhone|iPad|iPod/i.test(ua);
- const matchesModernIPad = platform === 'MacIntel' && typeof maxTouchPoints === 'number' && maxTouchPoints > 1;
+ const matchesClientHintsIOS = testPlatform(/^(iPhone|iPad|iPod|iOS)/i);
+ const matchesClassicIOS = testUserAgent(/iPhone|iPad|iPod/i);
- return matchesClassicIOS || matchesModernIPad;
+ return matchesClientHintsIOS || matchesClassicIOS || matchesModernIPad;
}
diff --git a/packages/react-simplikit/src/utils/isIOS/ja/isIOS.md b/packages/react-simplikit/src/utils/isIOS/ja/isIOS.md
index 67056f5e..f7443f5a 100644
--- a/packages/react-simplikit/src/utils/isIOS/ja/isIOS.md
+++ b/packages/react-simplikit/src/utils/isIOS/ja/isIOS.md
@@ -2,12 +2,15 @@
`isIOS` は、現在のデバイスで iOS または iPadOS が動作しているかどうかを判定するユーティリティ関数です。
+User-Agent Client Hints API(`navigator.userAgentData.platform`)を優先し、利用できない場合はレガシーな `navigator.platform` / `navigator.userAgent` 文字列にフォールバックします。
+
プラットフォーム情報の違いに関する補足:
- iPadOS 13 より前の iPad は、プラットフォームを「iPad」として報告していました(または UA が /iPad/ に一致していました)。
- iPadOS 13 以降、Apple はプラットフォーム文字列を「MacIntel」に変更し、
Web サイトが iPadOS をデスクトップ向けの Safari として扱うようにしました。
ただし、これらのデバイスでは引き続きマルチタッチ機能を検出できます。
+- Chromium 系ブラウザーは Client Hints の `platform` を `"iPhone"` ではなく `"iOS"` として報告する場合があります。
## インターフェース
@@ -20,7 +23,7 @@ function isIOS(userAgent?: string): boolean;
### 戻り値
@@ -28,20 +31,20 @@ function isIOS(userAgent?: string): boolean;
-## 使用例
+## 例
```tsx
if (isIOS()) {
- // iOS 固有のコード
+ // iOS 固有の処理
enableIOSOptimizations();
}
```
```tsx
-// カスタムのユーザーエージェントを指定する場合
+// ユーザーエージェントを直接渡す場合
const isIOSDevice = isIOS(
'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)'
);
diff --git a/packages/react-simplikit/src/utils/isIOS/ko/isIOS.md b/packages/react-simplikit/src/utils/isIOS/ko/isIOS.md
index c2ed6359..4f30b4e3 100644
--- a/packages/react-simplikit/src/utils/isIOS/ko/isIOS.md
+++ b/packages/react-simplikit/src/utils/isIOS/ko/isIOS.md
@@ -1,9 +1,14 @@
# isIOS
-`isIOS`는 현재 기기가 iOS 또는 iPadOS를 실행 중인지 감지하는 유틸리티 함수에요. 플랫폼에 따라 다음과 같은 차이가 있어요.
+`isIOS`는 현재 기기가 iOS 또는 iPadOS를 실행 중인지 감지하는 유틸리티 함수예요.
-- iPadOS 13 이전에는 iPads가 플랫폼을 'iPad'로 보고했었어요 (또는 UA에서 /iPad/와 일치함).
-- iPadOS 13부터 Apple은 웹 사이트가 iPadOS를 데스크톱급 Safari로 취급하게하기 위해 플랫폼 문자열을 'MacIntel'로 변경했어요. 그러나 이러한 기기는 여전히 멀티터치 기능을 노출해요.
+User-Agent Client Hints API(`navigator.userAgentData.platform`)를 우선 사용하고, 없으면 레거시 `navigator.platform` / `navigator.userAgent` 문자열로 판별해요.
+
+플랫폼에 따라 다음과 같은 차이가 있어요.
+
+- iPadOS 13 이전에는 iPad가 플랫폼을 'iPad'로 보고했어요 (또는 UA에서 /iPad/와 일치함).
+- iPadOS 13부터 Apple은 웹사이트가 iPadOS를 데스크톱급 Safari로 취급하도록 플랫폼 문자열을 'MacIntel'로 변경했어요. 그러나 이러한 기기는 여전히 멀티터치 기능을 노출해요.
+- Chromium 계열 브라우저는 Client Hints `platform`을 `"iPhone"` 대신 `"iOS"`로 보고할 수 있어요.
## 인터페이스
@@ -16,7 +21,7 @@ function isIOS(userAgent?: string): boolean;
### 반환 값
diff --git a/packages/react-simplikit/src/utils/isIOS/zh-Hans/isIOS.md b/packages/react-simplikit/src/utils/isIOS/zh-Hans/isIOS.md
index 7fcc99b8..b0095a3d 100644
--- a/packages/react-simplikit/src/utils/isIOS/zh-Hans/isIOS.md
+++ b/packages/react-simplikit/src/utils/isIOS/zh-Hans/isIOS.md
@@ -2,10 +2,13 @@
`isIOS` 是一个检测当前设备是否运行 iOS 或 iPadOS 的工具函数。
+优先使用 User-Agent Client Hints API(`navigator.userAgentData.platform`),不可用时回退到旧版 `navigator.platform` / `navigator.userAgent` 字符串。
+
关于平台不一致性的说明:
- 在 iPadOS 13 之前,iPad 申报的平台为“iPad”(或在 UA 中匹配 /iPad/)。
- 从 iPadOS 13 开始,Apple 将平台字符串改为“MacIntel”,让网站将 iPadOS 当作桌面级 Safari 对待。但这些设备仍然支持多点触控。
+- 基于 Chromium 的浏览器可能将 Client Hints 的 `platform` 报告为 `"iOS"` 而不是 `"iPhone"`。
## 接口
@@ -18,7 +21,7 @@ function isIOS(userAgent?: string): boolean;
### 返回值
@@ -26,20 +29,20 @@ function isIOS(userAgent?: string): boolean;
## 示例
```tsx
if (isIOS()) {
- // iOS-specific code
+ // iOS 专用代码
enableIOSOptimizations();
}
```
```tsx
-// With custom user agent
+// 传入自定义 user agent
const isIOSDevice = isIOS(
'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)'
);