Skip to content

Commit 6e821e7

Browse files
authored
feat!: extend browser, OS, device and in-app detection (#11)
BREAKING CHANGE: `isMac()` no longer returns `true` for iOS or iPadOS. Use the new `isiOS()` method instead. `deviceType()` now returns `TV`, `Console`, and `Wearable` in addition to the existing values. New browser detection: - isBrave(), isVivaldi(), isSamsungBrowser(), isArc(), isDuckDuckGo() New OS detection: - isiOS() (split from isMac), isChromeOS(), isHarmonyOS() New device type detection: - isTV(), isConsole(), isWearable() Expanded in-app detection: - Facebook, Instagram, TikTok, Snapchat, LinkedIn, Telegram, Line, Pinterest (in addition to existing WebView, Twitter, WeChat) Also fixes pre-existing issues: - iPadOS was not matched by any OS flag - Samsung Internet was misidentified as Chrome - Smart TVs, consoles, wearables all defaulted to Desktop - deviceType() used stringly-typed magic dispatch - parseVersion() produced dead version string overwritten downstream - skipBotDetection() called on every request instead of once - IE match used overly broad 'ie' substring (now word-boundary) - detectIsInApp() re-fetched agent from payload unnecessarily
1 parent 6746327 commit 6e821e7

11 files changed

Lines changed: 742 additions & 38 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Extended Browser, OS, Device & In-App Detection
2+
3+
## Goal
4+
5+
Surface more detection capabilities that matomo/device-detector already provides, and
6+
extend in-app browser detection to cover the most common social/messaging apps.
7+
8+
## Current state
9+
10+
- **Browsers:** Chrome, Firefox, Opera, Safari, Edge, IE
11+
- **OS:** Windows, Linux, Mac (incl. iOS), Android
12+
- **Device types:** Mobile, Tablet, Desktop, Bot
13+
- **In-app:** WebView, Twitter, WeChat, Apple in-app, Android WebView
14+
15+
## Proposed additions
16+
17+
### 1. Browser shortcuts (low effort)
18+
19+
matomo/device-detector already returns the browser family name. The `BrowserDetect` stage
20+
just needs more `elseif` branches + new boolean properties on `Result`.
21+
22+
| Method | Match on `browserFamily` |
23+
|---|---|
24+
| `isBrave()` | `Brave` |
25+
| `isVivaldi()` | `Vivaldi` |
26+
| `isSamsungBrowser()` | `Samsung Internet` |
27+
| `isArc()` | `Arc` |
28+
| `isDuckDuckGo()` | `DuckDuckGo Privacy Browser` |
29+
30+
### 2. OS shortcuts (low effort)
31+
32+
Same approach — match on `platformFamily`.
33+
34+
| Method | Match on `platformFamily` |
35+
|---|---|
36+
| `isChromeOS()` | `Chrome OS` |
37+
| `isHarmonyOS()` | `HarmonyOS` |
38+
| `isiOS()` | `iOS` (currently lumped into `isMac`) |
39+
40+
> Note: `isiOS()` is a breaking-ish change since `isMac()` currently returns true for iOS.
41+
> Consider keeping `isMac()` as-is for backwards compat and adding `isiOS()` alongside it.
42+
43+
### 3. Device type expansion (medium effort)
44+
45+
matomo/device-detector returns device types like `tv`, `console`, `wearable`,
46+
`smart speaker`, `car browser`, `smart display`, `camera`, `portable media player`,
47+
`peripheral`. Currently anything that isn't `desktop`, `tablet`, or `smartphone`/
48+
`feature phone`/`phablet` falls through and defaults to Desktop.
49+
50+
| Method | Device types mapped |
51+
|---|---|
52+
| `isTV()` | `tv` |
53+
| `isConsole()` | `console` |
54+
| `isWearable()` | `wearable` |
55+
56+
Also update `deviceType()` to return these new types instead of always falling back
57+
to `Desktop`.
58+
59+
### 4. In-app browser detection (medium effort)
60+
61+
Extend `detectIsInApp()` with user-agent substring checks for popular apps:
62+
63+
| App | UA substring |
64+
|---|---|
65+
| Facebook | `FBAN` or `FBAV` |
66+
| Instagram | `Instagram` |
67+
| TikTok | `BytedanceWebview` or `musical_ly` |
68+
| Snapchat | `Snapchat` |
69+
| LinkedIn | `LinkedInApp` |
70+
| Telegram | `TelegramBot` or `Telegram` |
71+
| Line | `Line/` |
72+
| Pinterest | `Pinterest` |
73+
74+
## Implementation order
75+
76+
1. **In-app detection** — standalone change in `BrowserDetect::detectIsInApp()`, no new
77+
properties needed, just better matching. Quick win.
78+
2. **Browser shortcuts** — add properties to `Result`, interface methods to
79+
`ResultInterface`, detection in `BrowserDetect` stage. Tests per browser.
80+
3. **OS shortcuts** — same pattern. Decide on `isiOS()` vs `isMac()` overlap.
81+
4. **Device types** — update `DeviceDetector` stage mapping, add properties, update
82+
`deviceType()` return values. This changes existing behavior (unknown devices
83+
currently default to Desktop), so it needs careful testing.
84+
85+
## Files to touch
86+
87+
- `src/Contracts/ResultInterface.php` — new method signatures
88+
- `src/Result.php` — new properties + methods
89+
- `src/Stages/BrowserDetect.php` — browser/OS matching + in-app detection
90+
- `src/Stages/DeviceDetector.php` — device type mapping
91+
- `tests/` — new test cases for each addition
92+
- `README.md` — update API reference table
93+
94+
## Open questions
95+
96+
- Should `isMac()` stop returning `true` for iOS once `isiOS()` exists? (breaking change)
97+
- Should device types like TV/Console/Wearable get their own Blade directives?
98+
- Should `isInApp()` expose _which_ app (e.g. `inAppName()`), or just remain boolean?

README.md

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ A Laravel package to identify the visitor's browser, operating system, and devic
1111

1212
> **Fork notice:** This is a maintained fork of [hisorange/browser-detect](https://github.com/hisorange/browser-detect) by [Varga Zsolt](https://github.com/hisorange), which appears to be abandoned. Full credit to the original author for the design and initial implementation. This fork keeps the package compatible with modern PHP and Laravel versions.
1313
14+
> **Upgrading to 6.x?** `isMac()` no longer returns `true` for iOS — use the new `isiOS()` method instead. See [UPGRADING.md](UPGRADING.md) for all breaking changes.
15+
1416
## Requirements
1517

1618
- PHP 8.3+
@@ -45,7 +47,7 @@ if (Browser::isFirefox() || Browser::isOpera()) {
4547

4648
if (Browser::isAndroid()) {
4749
$response .= '<a>Install our Android App!</a>';
48-
} elseif (Browser::isMac() && Browser::isMobile()) {
50+
} elseif (Browser::isiOS()) {
4951
$response .= '<a>Install our iOS App!</a>';
5052
}
5153
```
@@ -94,35 +96,43 @@ Results are cached in memory for the current request and optionally persisted vi
9496

9597
### Device detection
9698

97-
| Method | Returns | Description |
98-
| :------------------------ | :------: | :------------------------------------------- |
99-
| `Browser::isMobile()` | `bool` | Is this a mobile device? |
100-
| `Browser::isTablet()` | `bool` | Is this a tablet device? |
101-
| `Browser::isDesktop()` | `bool` | Is this a desktop computer? |
102-
| `Browser::isBot()` | `bool` | Is this a crawler / bot? |
103-
| `Browser::deviceType()` | `string` | One of: `Mobile`, `Tablet`, `Desktop`, `Bot` |
104-
| `Browser::deviceFamily()` | `string` | Device vendor (Samsung, Apple, Huawei, ...) |
105-
| `Browser::deviceModel()` | `string` | Device model (iPad, iPhone, Nexus, ...) |
99+
| Method | Returns | Description |
100+
| :------------------------ | :------: | :------------------------------------------------------------------------ |
101+
| `Browser::isMobile()` | `bool` | Is this a mobile device? |
102+
| `Browser::isTablet()` | `bool` | Is this a tablet device? |
103+
| `Browser::isDesktop()` | `bool` | Is this a desktop computer? |
104+
| `Browser::isBot()` | `bool` | Is this a crawler / bot? |
105+
| `Browser::isTV()` | `bool` | Is this a TV device? |
106+
| `Browser::isConsole()` | `bool` | Is this a game console? |
107+
| `Browser::isWearable()` | `bool` | Is this a wearable device? |
108+
| `Browser::deviceType()` | `string` | One of: `Mobile`, `Tablet`, `Desktop`, `Bot`, `TV`, `Console`, `Wearable` |
109+
| `Browser::deviceFamily()` | `string` | Device vendor (Samsung, Apple, Huawei, ...) |
110+
| `Browser::deviceModel()` | `string` | Device model (iPad, iPhone, Nexus, ...) |
106111

107112
### Browser detection
108113

109-
| Method | Returns | Description |
110-
| :------------------------------- | :------: | :------------------------------------------- |
111-
| `Browser::browserName()` | `string` | Human-friendly name (e.g. `Firefox 3.6`) |
112-
| `Browser::browserFamily()` | `string` | Vendor (Chrome, Firefox, Opera, ...) |
113-
| `Browser::browserVersion()` | `string` | Version string with trailing `.0` trimmed |
114-
| `Browser::browserVersionMajor()` | `int` | Semantic major version |
115-
| `Browser::browserVersionMinor()` | `int` | Semantic minor version |
116-
| `Browser::browserVersionPatch()` | `int` | Semantic patch version |
117-
| `Browser::browserEngine()` | `string` | Rendering engine (Blink, WebKit, Gecko, ...) |
118-
| `Browser::isChrome()` | `bool` | Chrome or Chromium? |
119-
| `Browser::isFirefox()` | `bool` | Firefox? |
120-
| `Browser::isOpera()` | `bool` | Opera? |
121-
| `Browser::isSafari()` | `bool` | Safari? |
122-
| `Browser::isEdge()` | `bool` | Microsoft Edge? |
123-
| `Browser::isIE()` | `bool` | Internet Explorer (or Trident)? |
124-
| `Browser::isIEVersion(int, op)` | `bool` | Compare against a specific IE version |
125-
| `Browser::isInApp()` | `bool` | In-app browser (WebView, Twitter, WeChat)? |
114+
| Method | Returns | Description |
115+
| :------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------- |
116+
| `Browser::browserName()` | `string` | Human-friendly name (e.g. `Firefox 3.6`) |
117+
| `Browser::browserFamily()` | `string` | Vendor (Chrome, Firefox, Opera, ...) |
118+
| `Browser::browserVersion()` | `string` | Version string with trailing `.0` trimmed |
119+
| `Browser::browserVersionMajor()` | `int` | Semantic major version |
120+
| `Browser::browserVersionMinor()` | `int` | Semantic minor version |
121+
| `Browser::browserVersionPatch()` | `int` | Semantic patch version |
122+
| `Browser::browserEngine()` | `string` | Rendering engine (Blink, WebKit, Gecko, ...) |
123+
| `Browser::isChrome()` | `bool` | Chrome or Chromium? |
124+
| `Browser::isFirefox()` | `bool` | Firefox? |
125+
| `Browser::isOpera()` | `bool` | Opera? |
126+
| `Browser::isSafari()` | `bool` | Safari? |
127+
| `Browser::isEdge()` | `bool` | Microsoft Edge? |
128+
| `Browser::isIE()` | `bool` | Internet Explorer (or Trident)? |
129+
| `Browser::isIEVersion(int, op)` | `bool` | Compare against a specific IE version |
130+
| `Browser::isBrave()` | `bool` | Brave? |
131+
| `Browser::isVivaldi()` | `bool` | Vivaldi? |
132+
| `Browser::isSamsungBrowser()` | `bool` | Samsung Internet? |
133+
| `Browser::isArc()` | `bool` | Arc? |
134+
| `Browser::isDuckDuckGo()` | `bool` | DuckDuckGo Privacy Browser? |
135+
| `Browser::isInApp()` | `bool` | In-app browser? (WebView, Twitter, WeChat, Facebook, Instagram, TikTok, Snapchat, LinkedIn, Telegram, Line, Pinterest) |
126136

127137
### Operating system detection
128138

@@ -136,7 +146,10 @@ Results are cached in memory for the current request and optionally persisted vi
136146
| `Browser::platformVersionPatch()` | `int` | Semantic patch version |
137147
| `Browser::isWindows()` | `bool` | Windows? |
138148
| `Browser::isLinux()` | `bool` | Linux? |
139-
| `Browser::isMac()` | `bool` | macOS or iOS? |
149+
| `Browser::isMac()` | `bool` | macOS? |
150+
| `Browser::isiOS()` | `bool` | iOS? |
151+
| `Browser::isChromeOS()` | `bool` | Chrome OS? |
152+
| `Browser::isHarmonyOS()` | `bool` | HarmonyOS? |
140153
| `Browser::isAndroid()` | `bool` | Android? |
141154

142155
## Configuration

UPGRADING.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Upgrading
2+
3+
## 6.x
4+
5+
### Breaking changes
6+
7+
- **`isMac()` no longer returns `true` for iOS or iPadOS.** A new `isiOS()`
8+
method now handles iOS and iPadOS detection separately. If your code uses
9+
`isMac()` to target both macOS and iOS, update it to check
10+
`isMac() || isiOS()`.
11+
- **`deviceType()` returns new values.** Previously unknown device types
12+
defaulted to `Desktop`. Now `TV`, `Console`, and `Wearable` are returned
13+
when detected. If you switch on `deviceType()`, add cases for these.
14+
15+
### New features
16+
17+
#### Browser detection
18+
19+
- `isBrave()` — Brave browser
20+
- `isVivaldi()` — Vivaldi browser
21+
- `isSamsungBrowser()` — Samsung Internet
22+
- `isArc()` — Arc browser
23+
- `isDuckDuckGo()` — DuckDuckGo Privacy Browser
24+
25+
#### OS detection
26+
27+
- `isiOS()` — iOS (previously covered by `isMac()`)
28+
- `isChromeOS()` — Chrome OS
29+
- `isHarmonyOS()` — HarmonyOS
30+
31+
#### Device type detection
32+
33+
- `isTV()` — Smart TVs, set-top boxes
34+
- `isConsole()` — Game consoles
35+
- `isWearable()` — Smartwatches, headsets
36+
37+
#### In-app browser detection
38+
39+
`isInApp()` now detects: Facebook, Instagram, TikTok, Snapchat, LinkedIn,
40+
Telegram, Line, and Pinterest (in addition to existing WebView, Twitter,
41+
and WeChat detection).

src/Contracts/ResultInterface.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function isTablet(): bool;
3737
public function isDesktop(): bool;
3838

3939
/**
40-
* What type of device is this: Mobile, Tablet, Desktop, Bot
40+
* What type of device is this: Mobile, Tablet, Desktop, Bot, TV, Console, Wearable
4141
*/
4242
public function deviceType(): string;
4343

@@ -46,6 +46,21 @@ public function deviceType(): string;
4646
*/
4747
public function isBot(): bool;
4848

49+
/**
50+
* Is this a TV device?
51+
*/
52+
public function isTV(): bool;
53+
54+
/**
55+
* Is this a game console?
56+
*/
57+
public function isConsole(): bool;
58+
59+
/**
60+
* Is this a wearable device?
61+
*/
62+
public function isWearable(): bool;
63+
4964
/**
5065
* Is this a Chrome or Chromium browser?
5166
*/
@@ -77,7 +92,32 @@ public function isEdge(): bool;
7792
public function isIE(): bool;
7893

7994
/**
80-
* Is this browser an android in-app browser?
95+
* Is this a Brave browser?
96+
*/
97+
public function isBrave(): bool;
98+
99+
/**
100+
* Is this a Vivaldi browser?
101+
*/
102+
public function isVivaldi(): bool;
103+
104+
/**
105+
* Is this a Samsung Internet browser?
106+
*/
107+
public function isSamsungBrowser(): bool;
108+
109+
/**
110+
* Is this an Arc browser?
111+
*/
112+
public function isArc(): bool;
113+
114+
/**
115+
* Is this a DuckDuckGo browser?
116+
*/
117+
public function isDuckDuckGo(): bool;
118+
119+
/**
120+
* Is this an in-app browser?
81121
*/
82122
public function isInApp(): bool;
83123

@@ -166,6 +206,21 @@ public function isLinux(): bool;
166206
*/
167207
public function isMac(): bool;
168208

209+
/**
210+
* Is this an iOS operating system?
211+
*/
212+
public function isiOS(): bool;
213+
214+
/**
215+
* Is this Chrome OS?
216+
*/
217+
public function isChromeOS(): bool;
218+
219+
/**
220+
* Is this HarmonyOS?
221+
*/
222+
public function isHarmonyOS(): bool;
223+
169224
/**
170225
* Is this an android operating system?
171226
*/

src/Facade.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,23 @@
1616
* @method static bool isSafari()
1717
* @method static bool isIE()
1818
* @method static bool isEdge()
19+
* @method static bool isBrave()
20+
* @method static bool isVivaldi()
21+
* @method static bool isSamsungBrowser()
22+
* @method static bool isArc()
23+
* @method static bool isDuckDuckGo()
1924
* @method static bool isWindows()
2025
* @method static bool isAndroid()
2126
* @method static bool isMac()
27+
* @method static bool isiOS()
28+
* @method static bool isChromeOS()
29+
* @method static bool isHarmonyOS()
2230
* @method static bool isLinux()
2331
* @method static bool isInApp()
2432
* @method static bool isBot()
33+
* @method static bool isTV()
34+
* @method static bool isConsole()
35+
* @method static bool isWearable()
2536
* @method static bool isIEVersion()
2637
* @method static string browserEngine()
2738
* @method static string browserName()

0 commit comments

Comments
 (0)