@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 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 (10)
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.147",
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.147",
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.147",
136
+ "@djangocfg/i18n": "^2.1.149",
130
137
  "@djangocfg/playground": "workspace:*",
131
- "@djangocfg/typescript-config": "^2.1.147",
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 translations
134
- const resolvedPlaceholder = placeholder ?? t('ui.select.placeholder')
135
- const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.select.search')
136
- const resolvedEmptyText = emptyText ?? t('ui.select.noResults')
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} {t('ui.table.selected')}
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={t('ui.actions.remove', { item: country.name })}
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
- {t('ui.select.moreItems', { count: remaining })}
321
+ {moreItemsLabel(remaining)}
318
322
  </Badge>
319
323
  )}
320
324
  </div>
321
325
  )
322
- }, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, t, multiple, handleRemove])
326
+ }, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel])
323
327
 
324
328
  return (
325
329
  <Popover
@@ -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
+ }