@djangocfg/ui-core 2.1.147 → 2.1.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/package.json +11 -4
- package/src/components/country-select.tsx +18 -14
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useBrowserDetect.ts +330 -0
- package/src/hooks/useDeviceDetect.ts +252 -0
- package/src/hooks/useHotkey.ts +105 -0
- package/src/lib/dialog-service/DialogProvider.tsx +115 -0
- package/src/lib/dialog-service/constants.ts +20 -0
- package/src/lib/dialog-service/dialog-service.story.tsx +263 -0
- package/src/lib/dialog-service/dialogs/AlertDialogUI.tsx +59 -0
- package/src/lib/dialog-service/dialogs/ConfirmDialogUI.tsx +81 -0
- package/src/lib/dialog-service/dialogs/PromptDialogUI.tsx +103 -0
- package/src/lib/dialog-service/dialogs/index.ts +3 -0
- package/src/lib/dialog-service/events.ts +73 -0
- package/src/lib/dialog-service/hooks/index.ts +1 -0
- package/src/lib/dialog-service/hooks/use-dialog.ts +79 -0
- package/src/lib/dialog-service/index.ts +16 -0
- package/src/lib/dialog-service/types.ts +59 -0
- package/src/lib/index.ts +1 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ pnpm add @djangocfg/ui-core
|
|
|
44
44
|
### Specialized (13)
|
|
45
45
|
`ButtonGroup` `Empty` `Spinner` `Preloader` `Kbd` `TokenIcon` `InputGroup` `Item` `ImageWithFallback` `OgImage` `CopyButton` `CopyField` `StaticPagination`
|
|
46
46
|
|
|
47
|
-
## Hooks (
|
|
47
|
+
## Hooks (13)
|
|
48
48
|
|
|
49
49
|
| Hook | Description |
|
|
50
50
|
|------|-------------|
|
|
@@ -58,6 +58,47 @@ pnpm add @djangocfg/ui-core
|
|
|
58
58
|
| `useToast` / `toast` | Toast notifications (Sonner) |
|
|
59
59
|
| `useEventListener` | Event bus |
|
|
60
60
|
| `useDebugTools` | Debug utilities |
|
|
61
|
+
| `useHotkey` | Keyboard shortcuts (react-hotkeys-hook) |
|
|
62
|
+
| `useBrowserDetect` | Browser detection (Chrome, Safari, in-app browsers, etc.) |
|
|
63
|
+
| `useDeviceDetect` | Device detection (mobile, tablet, desktop, OS, etc.) |
|
|
64
|
+
|
|
65
|
+
## Dialog Service
|
|
66
|
+
|
|
67
|
+
Universal dialog service to replace native `window.alert`, `window.confirm`, `window.prompt` with beautiful shadcn dialogs.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { DialogProvider, useDialog, dialog } from '@djangocfg/ui-core/lib/dialog-service';
|
|
71
|
+
|
|
72
|
+
// Wrap your app with DialogProvider
|
|
73
|
+
function App() {
|
|
74
|
+
return (
|
|
75
|
+
<DialogProvider>
|
|
76
|
+
<YourApp />
|
|
77
|
+
</DialogProvider>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Use via React hook
|
|
82
|
+
function Component() {
|
|
83
|
+
const { alert, confirm, prompt } = useDialog();
|
|
84
|
+
|
|
85
|
+
const handleDelete = async () => {
|
|
86
|
+
const confirmed = await confirm({
|
|
87
|
+
title: 'Delete item?',
|
|
88
|
+
message: 'This action cannot be undone.',
|
|
89
|
+
variant: 'destructive',
|
|
90
|
+
});
|
|
91
|
+
if (confirmed) {
|
|
92
|
+
// Delete...
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Or use globally from anywhere (vanilla JS, libraries, etc.)
|
|
98
|
+
dialog.alert({ message: 'Hello!' });
|
|
99
|
+
const ok = await dialog.confirm({ message: 'Are you sure?' });
|
|
100
|
+
const name = await dialog.prompt({ message: 'Enter your name:' });
|
|
101
|
+
```
|
|
61
102
|
|
|
62
103
|
## Usage
|
|
63
104
|
|
|
@@ -110,6 +151,7 @@ import '@djangocfg/ui-core/styles/globals';
|
|
|
110
151
|
| `@djangocfg/ui-core/components` | Components only |
|
|
111
152
|
| `@djangocfg/ui-core/hooks` | Hooks only |
|
|
112
153
|
| `@djangocfg/ui-core/lib` | Utilities (cn, etc.) |
|
|
154
|
+
| `@djangocfg/ui-core/lib/dialog-service` | Dialog service |
|
|
113
155
|
| `@djangocfg/ui-core/styles` | CSS |
|
|
114
156
|
|
|
115
157
|
## What's NOT included (use ui-nextjs)
|
|
@@ -122,7 +164,6 @@ These features require Next.js or browser storage APIs:
|
|
|
122
164
|
- `Pagination`, `SSRPagination` — uses next/link
|
|
123
165
|
- `DropdownMenu` — uses next/link
|
|
124
166
|
- `DownloadButton` — uses localStorage
|
|
125
|
-
- `useLocalStorage`, `useSessionStorage` — browser storage
|
|
126
167
|
- `useTheme` — uses next-themes
|
|
127
168
|
- `useQueryParams`, `useCfgRouter` — uses next/router
|
|
128
169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.149",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
"import": "./src/lib/index.ts",
|
|
49
49
|
"require": "./src/lib/index.ts"
|
|
50
50
|
},
|
|
51
|
+
"./lib/dialog-service": {
|
|
52
|
+
"types": "./src/lib/dialog-service/index.ts",
|
|
53
|
+
"import": "./src/lib/dialog-service/index.ts",
|
|
54
|
+
"require": "./src/lib/dialog-service/index.ts"
|
|
55
|
+
},
|
|
51
56
|
"./styles": "./src/styles/index.css",
|
|
52
57
|
"./styles/globals": "./src/styles/globals.css",
|
|
53
58
|
"./styles/theme": "./src/styles/theme.css",
|
|
@@ -66,7 +71,8 @@
|
|
|
66
71
|
"playground": "playground dev"
|
|
67
72
|
},
|
|
68
73
|
"peerDependencies": {
|
|
69
|
-
"@djangocfg/i18n": "^2.1.
|
|
74
|
+
"@djangocfg/i18n": "^2.1.149",
|
|
75
|
+
"react-device-detect": "^2.2.3",
|
|
70
76
|
"consola": "^3.4.2",
|
|
71
77
|
"lucide-react": "^0.545.0",
|
|
72
78
|
"moment": "^2.30.1",
|
|
@@ -122,13 +128,14 @@
|
|
|
122
128
|
"react-sticky-box": "^2.0.5",
|
|
123
129
|
"recharts": "2.15.4",
|
|
124
130
|
"sonner": "2.0.7",
|
|
131
|
+
"react-hotkeys-hook": "^4.6.1",
|
|
125
132
|
"tailwind-merge": "^3.3.1",
|
|
126
133
|
"vaul": "1.1.2"
|
|
127
134
|
},
|
|
128
135
|
"devDependencies": {
|
|
129
|
-
"@djangocfg/i18n": "^2.1.
|
|
136
|
+
"@djangocfg/i18n": "^2.1.149",
|
|
130
137
|
"@djangocfg/playground": "workspace:*",
|
|
131
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
138
|
+
"@djangocfg/typescript-config": "^2.1.149",
|
|
132
139
|
"@types/node": "^24.7.2",
|
|
133
140
|
"@types/react": "^19.1.0",
|
|
134
141
|
"@types/react-dom": "^19.1.0",
|
|
@@ -4,7 +4,6 @@ import { Check, ChevronsUpDown, Search, X } from 'lucide-react';
|
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import { countries, getEmojiFlag, type TCountryCode } from 'countries-list';
|
|
6
6
|
|
|
7
|
-
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
8
7
|
import { cn } from '../lib/utils';
|
|
9
8
|
import { Badge } from './badge';
|
|
10
9
|
import { Button } from './button';
|
|
@@ -34,11 +33,11 @@ export interface CountrySelectProps {
|
|
|
34
33
|
multiple?: boolean;
|
|
35
34
|
/** Display variant: dropdown (popover) or inline (scrollable list) */
|
|
36
35
|
variant?: CountrySelectVariant;
|
|
37
|
-
/** Placeholder text */
|
|
36
|
+
/** Placeholder text (default: "Select country...") */
|
|
38
37
|
placeholder?: string;
|
|
39
|
-
/** Search placeholder text */
|
|
38
|
+
/** Search placeholder text (default: "Search...") */
|
|
40
39
|
searchPlaceholder?: string;
|
|
41
|
-
/** Empty results text */
|
|
40
|
+
/** Empty results text (default: "No countries found") */
|
|
42
41
|
emptyText?: string;
|
|
43
42
|
/** Additional CSS class */
|
|
44
43
|
className?: string;
|
|
@@ -58,6 +57,10 @@ export interface CountrySelectProps {
|
|
|
58
57
|
maxHeight?: number;
|
|
59
58
|
/** Show search input */
|
|
60
59
|
showSearch?: boolean;
|
|
60
|
+
/** Custom label for selected count (receives count as param). Example: (count) => `${count} selected` */
|
|
61
|
+
selectedCountLabel?: (count: number) => string;
|
|
62
|
+
/** Custom label for "more items" badge (receives count as param). Example: (count) => `+${count} more` */
|
|
63
|
+
moreItemsLabel?: (count: number) => string;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/**
|
|
@@ -125,15 +128,16 @@ export function CountrySelect({
|
|
|
125
128
|
excludedCountries,
|
|
126
129
|
maxHeight = 300,
|
|
127
130
|
showSearch = true,
|
|
131
|
+
selectedCountLabel = (count: number) => `${count} selected`,
|
|
132
|
+
moreItemsLabel = (count: number) => `+${count} more`,
|
|
128
133
|
}: CountrySelectProps) {
|
|
129
|
-
const t = useTypedT<I18nTranslations>()
|
|
130
134
|
const [open, setOpen] = React.useState(false)
|
|
131
135
|
const [search, setSearch] = React.useState("")
|
|
132
136
|
|
|
133
|
-
// Resolve
|
|
134
|
-
const resolvedPlaceholder = placeholder ??
|
|
135
|
-
const resolvedSearchPlaceholder = searchPlaceholder ??
|
|
136
|
-
const resolvedEmptyText = emptyText ??
|
|
137
|
+
// Resolve defaults
|
|
138
|
+
const resolvedPlaceholder = placeholder ?? 'Select country...'
|
|
139
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? 'Search...'
|
|
140
|
+
const resolvedEmptyText = emptyText ?? 'No countries found'
|
|
137
141
|
|
|
138
142
|
// Build country options
|
|
139
143
|
const allCountries = React.useMemo<CountryOption[]>(() => {
|
|
@@ -214,12 +218,12 @@ export function CountrySelect({
|
|
|
214
218
|
{/* Selected count */}
|
|
215
219
|
{multiple && selectedCountries.length > 0 && (
|
|
216
220
|
<p className="text-sm text-muted-foreground">
|
|
217
|
-
{selectedCountries.length
|
|
221
|
+
{selectedCountLabel(selectedCountries.length)}
|
|
218
222
|
</p>
|
|
219
223
|
)}
|
|
220
224
|
|
|
221
225
|
{/* Country list */}
|
|
222
|
-
<ScrollArea style={{ maxHeight }} className="rounded-md border">
|
|
226
|
+
<ScrollArea style={{ height: maxHeight }} className="rounded-md border">
|
|
223
227
|
<div className="p-1">
|
|
224
228
|
{filteredCountries.length === 0 ? (
|
|
225
229
|
<p className="text-sm text-muted-foreground text-center py-4">
|
|
@@ -306,7 +310,7 @@ export function CountrySelect({
|
|
|
306
310
|
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
307
311
|
onClick={(e) => handleRemove(country.code, e)}
|
|
308
312
|
disabled={disabled}
|
|
309
|
-
aria-label={
|
|
313
|
+
aria-label={`Remove ${country.name}`}
|
|
310
314
|
>
|
|
311
315
|
<X className="h-3 w-3" />
|
|
312
316
|
</button>
|
|
@@ -314,12 +318,12 @@ export function CountrySelect({
|
|
|
314
318
|
))}
|
|
315
319
|
{remaining > 0 && (
|
|
316
320
|
<Badge variant="outline" className="text-xs">
|
|
317
|
-
{
|
|
321
|
+
{moreItemsLabel(remaining)}
|
|
318
322
|
</Badge>
|
|
319
323
|
)}
|
|
320
324
|
</div>
|
|
321
325
|
)
|
|
322
|
-
}, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled,
|
|
326
|
+
}, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel])
|
|
323
327
|
|
|
324
328
|
return (
|
|
325
329
|
<Popover
|
package/src/hooks/index.ts
CHANGED
|
@@ -19,3 +19,9 @@ export { useResolvedTheme } from './useResolvedTheme';
|
|
|
19
19
|
export type { ResolvedTheme } from './useResolvedTheme';
|
|
20
20
|
export { useLocalStorage } from './useLocalStorage';
|
|
21
21
|
export { useSessionStorage } from './useSessionStorage';
|
|
22
|
+
export { useHotkey, useHotkeysContext, HotkeysProvider, isHotkeyPressed } from './useHotkey';
|
|
23
|
+
export type { UseHotkeyOptions, HotkeyCallback, Keys, HotkeyRefType } from './useHotkey';
|
|
24
|
+
export { useBrowserDetect } from './useBrowserDetect';
|
|
25
|
+
export type { BrowserInfo } from './useBrowserDetect';
|
|
26
|
+
export { useDeviceDetect } from './useDeviceDetect';
|
|
27
|
+
export type { DeviceDetectResult } from './useDeviceDetect';
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced browser detection hook
|
|
3
|
+
*
|
|
4
|
+
* Detects modern browsers including Chromium-based browsers that may
|
|
5
|
+
* incorrectly report as Safari (Arc, Brave, Vivaldi, Comet, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useMemo } from 'react';
|
|
11
|
+
|
|
12
|
+
export interface BrowserInfo {
|
|
13
|
+
// Core browser types
|
|
14
|
+
isChrome: boolean;
|
|
15
|
+
isChromium: boolean; // Any Chromium-based browser
|
|
16
|
+
isSafari: boolean; // Real Safari (WebKit on macOS/iOS)
|
|
17
|
+
isFirefox: boolean;
|
|
18
|
+
isEdge: boolean;
|
|
19
|
+
isOpera: boolean;
|
|
20
|
+
|
|
21
|
+
// Modern Chromium-based browsers
|
|
22
|
+
isBrave: boolean;
|
|
23
|
+
isArc: boolean;
|
|
24
|
+
isVivaldi: boolean;
|
|
25
|
+
isYandex: boolean;
|
|
26
|
+
isSamsungBrowser: boolean;
|
|
27
|
+
isUCBrowser: boolean;
|
|
28
|
+
|
|
29
|
+
// Additional browsers
|
|
30
|
+
isComet: boolean; // Perplexity's Comet browser (Chromium-based, supports push)
|
|
31
|
+
isOperaMini: boolean; // Opera Mini (does NOT support push notifications)
|
|
32
|
+
isIE: boolean; // Internet Explorer (does NOT support push notifications)
|
|
33
|
+
|
|
34
|
+
// In-App Browsers (WebViews) - typically do NOT support push notifications
|
|
35
|
+
isFacebookInApp: boolean; // Facebook's in-app browser
|
|
36
|
+
isInstagramInApp: boolean; // Instagram's in-app browser
|
|
37
|
+
isTikTokInApp: boolean; // TikTok's in-app browser
|
|
38
|
+
isSnapchatInApp: boolean; // Snapchat's in-app browser
|
|
39
|
+
isWeChatInApp: boolean; // WeChat's in-app browser
|
|
40
|
+
isThreadsInApp: boolean; // Threads' in-app browser
|
|
41
|
+
isLinkedInInApp: boolean; // LinkedIn's in-app browser (uses Chrome WebView - supports push on Android)
|
|
42
|
+
isTwitterInApp: boolean; // Twitter/X's in-app browser (uses Chrome WebView - supports push on Android)
|
|
43
|
+
isInAppBrowser: boolean; // Any in-app browser detected
|
|
44
|
+
isWebView: boolean; // Generic WebView detection
|
|
45
|
+
|
|
46
|
+
// Browser name
|
|
47
|
+
browserName: string;
|
|
48
|
+
|
|
49
|
+
// Engine
|
|
50
|
+
isWebKit: boolean; // Safari's engine
|
|
51
|
+
isBlink: boolean; // Chromium's engine
|
|
52
|
+
isGecko: boolean; // Firefox's engine
|
|
53
|
+
|
|
54
|
+
// Push notification support
|
|
55
|
+
/**
|
|
56
|
+
* Whether the browser supports Web Push Notifications.
|
|
57
|
+
* Returns false for browsers known to NOT support push:
|
|
58
|
+
* - Opera Mini (no service worker support)
|
|
59
|
+
* - Internet Explorer (deprecated, no Push API)
|
|
60
|
+
* - UC Browser (unreliable push support)
|
|
61
|
+
* - In-App browsers (Facebook, Instagram, TikTok, Snapchat, etc.)
|
|
62
|
+
* - Generic WebViews (except Twitter/LinkedIn on Android which use Chrome WebView)
|
|
63
|
+
*
|
|
64
|
+
* Note: Comet (Perplexity) is Chromium-based and DOES support push notifications.
|
|
65
|
+
* Note: This is a browser-level check. For full push support,
|
|
66
|
+
* also check 'serviceWorker' in navigator && 'PushManager' in window
|
|
67
|
+
*/
|
|
68
|
+
supportsPushNotifications: boolean;
|
|
69
|
+
|
|
70
|
+
// iOS specific
|
|
71
|
+
isIOSBrowser: boolean; // Any browser on iOS (all use WebKit, limited push support)
|
|
72
|
+
|
|
73
|
+
// For debugging
|
|
74
|
+
userAgent: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const defaultBrowserInfo: BrowserInfo = {
|
|
78
|
+
isChrome: false,
|
|
79
|
+
isChromium: false,
|
|
80
|
+
isSafari: false,
|
|
81
|
+
isFirefox: false,
|
|
82
|
+
isEdge: false,
|
|
83
|
+
isOpera: false,
|
|
84
|
+
isBrave: false,
|
|
85
|
+
isArc: false,
|
|
86
|
+
isVivaldi: false,
|
|
87
|
+
isYandex: false,
|
|
88
|
+
isSamsungBrowser: false,
|
|
89
|
+
isUCBrowser: false,
|
|
90
|
+
isComet: false,
|
|
91
|
+
isOperaMini: false,
|
|
92
|
+
isIE: false,
|
|
93
|
+
isFacebookInApp: false,
|
|
94
|
+
isInstagramInApp: false,
|
|
95
|
+
isTikTokInApp: false,
|
|
96
|
+
isSnapchatInApp: false,
|
|
97
|
+
isWeChatInApp: false,
|
|
98
|
+
isThreadsInApp: false,
|
|
99
|
+
isLinkedInInApp: false,
|
|
100
|
+
isTwitterInApp: false,
|
|
101
|
+
isInAppBrowser: false,
|
|
102
|
+
isWebView: false,
|
|
103
|
+
browserName: 'unknown',
|
|
104
|
+
isWebKit: false,
|
|
105
|
+
isBlink: false,
|
|
106
|
+
isGecko: false,
|
|
107
|
+
supportsPushNotifications: false,
|
|
108
|
+
isIOSBrowser: false,
|
|
109
|
+
userAgent: '',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect browser with improved accuracy for Chromium-based browsers
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* const browser = useBrowserDetect();
|
|
118
|
+
*
|
|
119
|
+
* if (browser.isSafari && !browser.isChromium) {
|
|
120
|
+
* // Real Safari
|
|
121
|
+
* }
|
|
122
|
+
*
|
|
123
|
+
* if (browser.isChromium) {
|
|
124
|
+
* // Any Chromium-based browser (Chrome, Edge, Brave, Arc, etc.)
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function useBrowserDetect(): BrowserInfo {
|
|
129
|
+
return useMemo(() => {
|
|
130
|
+
if (typeof window === 'undefined') {
|
|
131
|
+
return defaultBrowserInfo;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ua = window.navigator.userAgent.toLowerCase();
|
|
135
|
+
|
|
136
|
+
// Check for specific browsers first (most specific to least specific)
|
|
137
|
+
|
|
138
|
+
// Edge (Chromium-based)
|
|
139
|
+
const isEdge = ua.includes('edg/') || ua.includes('edge/');
|
|
140
|
+
|
|
141
|
+
// Brave (check for Brave-specific API)
|
|
142
|
+
const isBrave = !!(window.navigator as any).brave;
|
|
143
|
+
|
|
144
|
+
// Arc (check for Arc-specific markers in UA)
|
|
145
|
+
const isArc = ua.includes('arc/');
|
|
146
|
+
|
|
147
|
+
// Vivaldi
|
|
148
|
+
const isVivaldi = ua.includes('vivaldi');
|
|
149
|
+
|
|
150
|
+
// Yandex Browser
|
|
151
|
+
const isYandex = ua.includes('yabrowser');
|
|
152
|
+
|
|
153
|
+
// Samsung Internet
|
|
154
|
+
const isSamsungBrowser = ua.includes('samsungbrowser');
|
|
155
|
+
|
|
156
|
+
// UC Browser
|
|
157
|
+
const isUCBrowser = ua.includes('ucbrowser') || ua.includes('uc browser');
|
|
158
|
+
|
|
159
|
+
// Comet Browser (Perplexity's AI browser, Chromium-based)
|
|
160
|
+
const isComet = ua.includes('comet') || ua.includes('perplexity');
|
|
161
|
+
|
|
162
|
+
// Opera Mini (does NOT support service workers or push)
|
|
163
|
+
const isOperaMini = ua.includes('opera mini') || ua.includes('opios');
|
|
164
|
+
|
|
165
|
+
// Internet Explorer (deprecated, no Push API support)
|
|
166
|
+
const isIE = ua.includes('msie') || ua.includes('trident/');
|
|
167
|
+
|
|
168
|
+
// In-App Browsers Detection
|
|
169
|
+
const isFacebookInApp = ua.includes('fban') || ua.includes('fbav') || ua.includes('fb_iab');
|
|
170
|
+
const isInstagramInApp = ua.includes('instagram');
|
|
171
|
+
const isTikTokInApp = ua.includes('tiktok') || ua.includes('bytedancewebview') || ua.includes('bytelocale');
|
|
172
|
+
const isSnapchatInApp = ua.includes('snapchat');
|
|
173
|
+
const isWeChatInApp = ua.includes('micromessenger');
|
|
174
|
+
const isThreadsInApp = ua.includes('barcelona');
|
|
175
|
+
const isLinkedInInApp = ua.includes('linkedinapp');
|
|
176
|
+
const isTwitterInApp = ua.includes('twitter');
|
|
177
|
+
const isPinterestInApp = ua.includes('pinterest');
|
|
178
|
+
const isTelegramInApp = ua.includes('telegram');
|
|
179
|
+
const isLineInApp = ua.includes('line/');
|
|
180
|
+
const isKakaoInApp = ua.includes('kakaotalk');
|
|
181
|
+
|
|
182
|
+
// Generic WebView detection
|
|
183
|
+
const isWebView = ua.includes('wv)') ||
|
|
184
|
+
ua.includes('webview') ||
|
|
185
|
+
ua.includes('; wv') ||
|
|
186
|
+
(ua.includes('iphone') && !ua.includes('safari')) ||
|
|
187
|
+
(ua.includes('ipad') && !ua.includes('safari'));
|
|
188
|
+
|
|
189
|
+
// Aggregate: Any in-app browser
|
|
190
|
+
const isInAppBrowser = isFacebookInApp ||
|
|
191
|
+
isInstagramInApp ||
|
|
192
|
+
isTikTokInApp ||
|
|
193
|
+
isSnapchatInApp ||
|
|
194
|
+
isWeChatInApp ||
|
|
195
|
+
isThreadsInApp ||
|
|
196
|
+
isLinkedInInApp ||
|
|
197
|
+
isTwitterInApp ||
|
|
198
|
+
isPinterestInApp ||
|
|
199
|
+
isTelegramInApp ||
|
|
200
|
+
isLineInApp ||
|
|
201
|
+
isKakaoInApp;
|
|
202
|
+
|
|
203
|
+
// iOS detection
|
|
204
|
+
const isIOSBrowser = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod');
|
|
205
|
+
|
|
206
|
+
// Opera (modern Chromium-based)
|
|
207
|
+
const isOpera = (ua.includes('opr/') || ua.includes('opera')) && !isOperaMini;
|
|
208
|
+
|
|
209
|
+
// Chrome
|
|
210
|
+
const isChrome = ua.includes('chrome') &&
|
|
211
|
+
!isEdge &&
|
|
212
|
+
!isOpera &&
|
|
213
|
+
!isYandex &&
|
|
214
|
+
!isSamsungBrowser &&
|
|
215
|
+
!isVivaldi &&
|
|
216
|
+
!isArc &&
|
|
217
|
+
!isBrave &&
|
|
218
|
+
!isComet;
|
|
219
|
+
|
|
220
|
+
// Firefox
|
|
221
|
+
const isFirefox = ua.includes('firefox') && !ua.includes('seamonkey');
|
|
222
|
+
|
|
223
|
+
// Safari (real Safari)
|
|
224
|
+
const hasSafariUA = ua.includes('safari');
|
|
225
|
+
const hasChrome = ua.includes('chrome') || ua.includes('crios');
|
|
226
|
+
const hasVersion = ua.includes('version/');
|
|
227
|
+
const isSafari = hasSafariUA && !hasChrome && hasVersion;
|
|
228
|
+
|
|
229
|
+
// Chromium detection
|
|
230
|
+
const isChromium = hasChrome ||
|
|
231
|
+
isEdge ||
|
|
232
|
+
isOpera ||
|
|
233
|
+
isYandex ||
|
|
234
|
+
isSamsungBrowser ||
|
|
235
|
+
isVivaldi ||
|
|
236
|
+
isArc ||
|
|
237
|
+
isBrave ||
|
|
238
|
+
isUCBrowser ||
|
|
239
|
+
isComet;
|
|
240
|
+
|
|
241
|
+
// Engine detection
|
|
242
|
+
const isWebKit = !isChromium && isSafari;
|
|
243
|
+
const isBlink = isChromium;
|
|
244
|
+
const isGecko = isFirefox;
|
|
245
|
+
|
|
246
|
+
// Determine browser name
|
|
247
|
+
let browserName = 'unknown';
|
|
248
|
+
if (isFacebookInApp) browserName = 'Facebook In-App';
|
|
249
|
+
else if (isInstagramInApp) browserName = 'Instagram In-App';
|
|
250
|
+
else if (isTikTokInApp) browserName = 'TikTok In-App';
|
|
251
|
+
else if (isSnapchatInApp) browserName = 'Snapchat In-App';
|
|
252
|
+
else if (isWeChatInApp) browserName = 'WeChat In-App';
|
|
253
|
+
else if (isThreadsInApp) browserName = 'Threads In-App';
|
|
254
|
+
else if (isLinkedInInApp) browserName = 'LinkedIn In-App';
|
|
255
|
+
else if (isTwitterInApp) browserName = 'Twitter In-App';
|
|
256
|
+
else if (isPinterestInApp) browserName = 'Pinterest In-App';
|
|
257
|
+
else if (isTelegramInApp) browserName = 'Telegram In-App';
|
|
258
|
+
else if (isLineInApp) browserName = 'Line In-App';
|
|
259
|
+
else if (isKakaoInApp) browserName = 'KakaoTalk In-App';
|
|
260
|
+
else if (isComet) browserName = 'Comet';
|
|
261
|
+
else if (isOperaMini) browserName = 'Opera Mini';
|
|
262
|
+
else if (isIE) browserName = 'Internet Explorer';
|
|
263
|
+
else if (isBrave) browserName = 'Brave';
|
|
264
|
+
else if (isArc) browserName = 'Arc';
|
|
265
|
+
else if (isVivaldi) browserName = 'Vivaldi';
|
|
266
|
+
else if (isYandex) browserName = 'Yandex';
|
|
267
|
+
else if (isSamsungBrowser) browserName = 'Samsung Internet';
|
|
268
|
+
else if (isUCBrowser) browserName = 'UC Browser';
|
|
269
|
+
else if (isEdge) browserName = 'Edge';
|
|
270
|
+
else if (isOpera) browserName = 'Opera';
|
|
271
|
+
else if (isChrome) browserName = 'Chrome';
|
|
272
|
+
else if (isSafari) browserName = 'Safari';
|
|
273
|
+
else if (isFirefox) browserName = 'Firefox';
|
|
274
|
+
else if (isWebView) browserName = 'WebView';
|
|
275
|
+
|
|
276
|
+
// Determine push notification support
|
|
277
|
+
const browserBlocksPush = isOperaMini ||
|
|
278
|
+
isIE ||
|
|
279
|
+
isUCBrowser ||
|
|
280
|
+
isFacebookInApp ||
|
|
281
|
+
isInstagramInApp ||
|
|
282
|
+
isTikTokInApp ||
|
|
283
|
+
isSnapchatInApp ||
|
|
284
|
+
isWeChatInApp ||
|
|
285
|
+
isThreadsInApp ||
|
|
286
|
+
isPinterestInApp ||
|
|
287
|
+
isTelegramInApp ||
|
|
288
|
+
isLineInApp ||
|
|
289
|
+
isKakaoInApp ||
|
|
290
|
+
isWebView;
|
|
291
|
+
|
|
292
|
+
const twitterLinkedInAndroid = (isTwitterInApp || isLinkedInInApp) && !isIOSBrowser;
|
|
293
|
+
const supportsPushNotifications = !browserBlocksPush || twitterLinkedInAndroid;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
isChrome,
|
|
297
|
+
isChromium,
|
|
298
|
+
isSafari,
|
|
299
|
+
isFirefox,
|
|
300
|
+
isEdge,
|
|
301
|
+
isOpera,
|
|
302
|
+
isBrave,
|
|
303
|
+
isArc,
|
|
304
|
+
isVivaldi,
|
|
305
|
+
isYandex,
|
|
306
|
+
isSamsungBrowser,
|
|
307
|
+
isUCBrowser,
|
|
308
|
+
isComet,
|
|
309
|
+
isOperaMini,
|
|
310
|
+
isIE,
|
|
311
|
+
isFacebookInApp,
|
|
312
|
+
isInstagramInApp,
|
|
313
|
+
isTikTokInApp,
|
|
314
|
+
isSnapchatInApp,
|
|
315
|
+
isWeChatInApp,
|
|
316
|
+
isThreadsInApp,
|
|
317
|
+
isLinkedInInApp,
|
|
318
|
+
isTwitterInApp,
|
|
319
|
+
isInAppBrowser,
|
|
320
|
+
isWebView,
|
|
321
|
+
browserName,
|
|
322
|
+
isWebKit,
|
|
323
|
+
isBlink,
|
|
324
|
+
isGecko,
|
|
325
|
+
supportsPushNotifications,
|
|
326
|
+
isIOSBrowser,
|
|
327
|
+
userAgent: window.navigator.userAgent,
|
|
328
|
+
};
|
|
329
|
+
}, []);
|
|
330
|
+
}
|