@omega.js/client 0.1.0
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/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// Methods are defined as arrow class fields so `this` is permanently bound to the instance.
|
|
2
|
+
// This means consumers can safely alias or destructure methods without losing context:
|
|
3
|
+
// const { escapeHTML } = omega.utilities(); // ✓ works
|
|
4
|
+
// const escape = omega.utilities().escapeHTML; // ✓ works
|
|
5
|
+
// items.map(omega.utilities().escapeHTML); // ✓ works
|
|
6
|
+
// Safe because omega.utilities() is a singleton — only one instance ever exists.
|
|
7
|
+
|
|
8
|
+
// renderMarkdown links are restricted to the two schemes a browser may navigate
|
|
9
|
+
// safely. sanitizeURL already rejects javascript:/data:, but it resolves a bare
|
|
10
|
+
// path against the current origin and returns it — and the bracket syntax is the
|
|
11
|
+
// one place the source supplies an attribute VALUE rather than text, so a link is
|
|
12
|
+
// only ever minted from a URL that says its own scheme out loud.
|
|
13
|
+
const SAFE_HREF = /^https?:\/\//i;
|
|
14
|
+
|
|
15
|
+
// The inline grammar, applied to an already-escaped line.
|
|
16
|
+
const renderInline = (text, sanitizeURL) => text
|
|
17
|
+
// Code first: what is inside a span of backticks is literal, and running the
|
|
18
|
+
// emphasis rules over it would eat the asterisks in a code sample.
|
|
19
|
+
.split(/(`[^`]+`)/)
|
|
20
|
+
.map((part) => {
|
|
21
|
+
if (part.startsWith('`') && part.endsWith('`') && part.length > 1) {
|
|
22
|
+
return `<code>${part.slice(1, -1)}</code>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Built anchors are stashed behind a NUL sentinel while the emphasis rules
|
|
26
|
+
// run — an href may legitimately contain asterisks, and the emphasis pass
|
|
27
|
+
// must never see markup it built.
|
|
28
|
+
const anchors = [];
|
|
29
|
+
|
|
30
|
+
return part
|
|
31
|
+
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (whole, label, href) => {
|
|
32
|
+
const safe = sanitizeURL(href);
|
|
33
|
+
|
|
34
|
+
// Not a scheme a browser may follow — leave the bracket text as text.
|
|
35
|
+
if (!safe || !SAFE_HREF.test(safe)) {
|
|
36
|
+
return whole;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
anchors.push(`<a href="${safe}" target="_blank" rel="noopener">${label}</a>`);
|
|
40
|
+
return `\u0000${anchors.length - 1}\u0000`;
|
|
41
|
+
})
|
|
42
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
43
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
|
|
44
|
+
.replace(/\u0000(\d+)\u0000/g, (match, index) => anchors[Number(index)]);
|
|
45
|
+
})
|
|
46
|
+
.join('');
|
|
47
|
+
|
|
48
|
+
class Utilities {
|
|
49
|
+
constructor(manager) {
|
|
50
|
+
this.manager = manager;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Copy text to clipboard
|
|
54
|
+
//
|
|
55
|
+
// Always a promise, and a REFUSED copy REJECTS
|
|
56
|
+
// ([#726](https://github.com/Omega-JS-Stack/omega/issues/726)). Every caller
|
|
57
|
+
// draws its confirmation off this promise, so a failure that resolves tells
|
|
58
|
+
// the visitor their credential is in the buffer when it is not.
|
|
59
|
+
clipboardCopy = async (input) => {
|
|
60
|
+
// Get the text from the input
|
|
61
|
+
const text = input && input.nodeType
|
|
62
|
+
? input.value || input.innerText || input.innerHTML
|
|
63
|
+
: input;
|
|
64
|
+
|
|
65
|
+
// Try to use the modern clipboard API — a refusal (no permission, an
|
|
66
|
+
// insecure context, a blurred document) is not a failure until the legacy
|
|
67
|
+
// lane has had its turn too.
|
|
68
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
69
|
+
try {
|
|
70
|
+
return await navigator.clipboard.writeText(text);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return fallbackCopy(text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return fallbackCopy(text);
|
|
77
|
+
|
|
78
|
+
function fallbackCopy(text) {
|
|
79
|
+
const el = document.createElement('textarea');
|
|
80
|
+
el.setAttribute('style', 'width:1px;border:0;opacity:0;');
|
|
81
|
+
el.value = text;
|
|
82
|
+
document.body.appendChild(el);
|
|
83
|
+
el.select();
|
|
84
|
+
|
|
85
|
+
// `execCommand` reports a refusal with a FALSE RETURN rather than a
|
|
86
|
+
// throw — reading the return is the only way this lane can fail.
|
|
87
|
+
let copied = false;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
copied = document.execCommand('copy');
|
|
91
|
+
} finally {
|
|
92
|
+
document.body.removeChild(el);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!copied) {
|
|
96
|
+
throw new Error('Failed to copy to clipboard');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Escape HTML to prevent XSS
|
|
102
|
+
// Accepts a string, object, or array — walks recursively, escaping all string values
|
|
103
|
+
escapeHTML = (input) => {
|
|
104
|
+
// Strings — escape and return
|
|
105
|
+
if (typeof input === 'string') {
|
|
106
|
+
this._shadowElement = this._shadowElement || document.createElement('p');
|
|
107
|
+
this._shadowElement.innerHTML = '';
|
|
108
|
+
|
|
109
|
+
// This automatically escapes HTML entities like <, >, &, etc.
|
|
110
|
+
this._shadowElement.appendChild(document.createTextNode(input));
|
|
111
|
+
|
|
112
|
+
// This is needed to escape quotes to prevent attribute injection
|
|
113
|
+
return this._shadowElement.innerHTML.replace(/["']/g, (m) => {
|
|
114
|
+
switch (m) {
|
|
115
|
+
case '"':
|
|
116
|
+
return '"';
|
|
117
|
+
default:
|
|
118
|
+
return ''';
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Null/undefined — pass through
|
|
124
|
+
if (input == null) {
|
|
125
|
+
return input;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Arrays — recurse each item
|
|
129
|
+
if (Array.isArray(input)) {
|
|
130
|
+
return input.map(item => this.escapeHTML(item));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Objects — shallow clone, recurse each value
|
|
134
|
+
if (typeof input === 'object') {
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const [key, value] of Object.entries(input)) {
|
|
137
|
+
result[key] = this.escapeHTML(value);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Numbers, booleans, etc. — pass through unchanged
|
|
143
|
+
return input;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sanitize URL to prevent javascript:, data:, and other dangerous URI schemes
|
|
147
|
+
// Returns the original URL if safe, or '' if rejected
|
|
148
|
+
sanitizeURL = (url) => {
|
|
149
|
+
if (!url || typeof url !== 'string') {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const parsed = new URL(url, window.location.origin);
|
|
155
|
+
|
|
156
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return url;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Render hostile text as safe markup with a small markdown grammar: headings,
|
|
167
|
+
// fenced and inline code, lists, bold/italic, and links restricted to http(s).
|
|
168
|
+
//
|
|
169
|
+
// The input is untrusted (an API answer, another user's words), so nothing here
|
|
170
|
+
// ever passes markup through: the text is ESCAPED FIRST, once, and every rule
|
|
171
|
+
// below works on that escaped string — a `<script>` is already `<script>`
|
|
172
|
+
// before any rule decides what a line means, so no rule can resurrect it.
|
|
173
|
+
// Escaping is escapeHTML's job and scheme safety is sanitizeURL's; this method
|
|
174
|
+
// only decides what a line MEANS.
|
|
175
|
+
//
|
|
176
|
+
// Not a markdown engine and not trying to be one — anything outside the grammar
|
|
177
|
+
// renders as the text it was. Empty input renders as '', so the caller can say
|
|
178
|
+
// what empty means in its own words.
|
|
179
|
+
renderMarkdown = (text) => {
|
|
180
|
+
const source = String(text === null || text === undefined ? '' : text);
|
|
181
|
+
if (!source.trim()) {
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines = this.escapeHTML(source.replace(/\r\n/g, '\n')).split('\n');
|
|
186
|
+
const out = [];
|
|
187
|
+
let paragraph = [];
|
|
188
|
+
let list = null;
|
|
189
|
+
let code = null;
|
|
190
|
+
|
|
191
|
+
const closeParagraph = () => {
|
|
192
|
+
if (!paragraph.length) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
out.push(`<p>${renderInline(paragraph.join('<br>'), this.sanitizeURL)}</p>`);
|
|
197
|
+
paragraph = [];
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const closeList = () => {
|
|
201
|
+
if (!list) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const items = list.items.map((item) => `<li>${renderInline(item, this.sanitizeURL)}</li>`).join('');
|
|
206
|
+
out.push(`<${list.tag}>${items}</${list.tag}>`);
|
|
207
|
+
list = null;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const closeBlocks = () => {
|
|
211
|
+
closeParagraph();
|
|
212
|
+
closeList();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
// A fence swallows everything until the next one — inside it, no rule but
|
|
217
|
+
// "this is literal" applies.
|
|
218
|
+
const fence = /^\s*```/.test(line);
|
|
219
|
+
if (code !== null) {
|
|
220
|
+
if (fence) {
|
|
221
|
+
out.push(`<pre class="p-2 rounded"><code>${code.join('\n')}</code></pre>`);
|
|
222
|
+
code = null;
|
|
223
|
+
} else {
|
|
224
|
+
code.push(line);
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (fence) {
|
|
229
|
+
closeBlocks();
|
|
230
|
+
code = [];
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
235
|
+
if (heading) {
|
|
236
|
+
closeBlocks();
|
|
237
|
+
|
|
238
|
+
// Rendered text is a fragment inside a host page, not a document: its
|
|
239
|
+
// headings start below the host's own title rather than competing with it.
|
|
240
|
+
const level = Math.min(heading[1].length + 3, 6);
|
|
241
|
+
out.push(`<h${level} class="h6 mt-3 mb-2">${renderInline(heading[2], this.sanitizeURL)}</h${level}>`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const bullet = line.match(/^\s*[-*]\s+(.*)$/);
|
|
246
|
+
const numbered = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
247
|
+
if (bullet || numbered) {
|
|
248
|
+
closeParagraph();
|
|
249
|
+
|
|
250
|
+
const tag = bullet ? 'ul' : 'ol';
|
|
251
|
+
if (list && list.tag !== tag) {
|
|
252
|
+
closeList();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
list = list || { tag, items: [] };
|
|
256
|
+
list.items.push((bullet || numbered)[1]);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!line.trim()) {
|
|
261
|
+
closeBlocks();
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
closeList();
|
|
266
|
+
paragraph.push(line);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// An unterminated fence is still content — render what it holds rather than
|
|
270
|
+
// dropping the rest of the text on the floor.
|
|
271
|
+
if (code !== null) {
|
|
272
|
+
out.push(`<pre class="p-2 rounded"><code>${code.join('\n')}</code></pre>`);
|
|
273
|
+
}
|
|
274
|
+
closeBlocks();
|
|
275
|
+
|
|
276
|
+
return out.join('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Show notification
|
|
280
|
+
showNotification = (message, options = {}) => {
|
|
281
|
+
// Handle different input types
|
|
282
|
+
let text = message;
|
|
283
|
+
let type = options.type || 'info';
|
|
284
|
+
|
|
285
|
+
// If message is an Error object, extract message and default to danger
|
|
286
|
+
if (message instanceof Error) {
|
|
287
|
+
text = message.message;
|
|
288
|
+
type = options.type || 'danger';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Handle string as second parameter for backwards compatibility
|
|
292
|
+
if (typeof options === 'string') {
|
|
293
|
+
options = { type: options };
|
|
294
|
+
type = options.type;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract options
|
|
298
|
+
const timeout = options.timeout !== undefined ? options.timeout : 5000;
|
|
299
|
+
|
|
300
|
+
const $notification = document.createElement('div');
|
|
301
|
+
$notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
302
|
+
$notification.style.cssText = 'z-index: 9999; top: 1rem; left: 50%; transform: translateX(-50%); width: calc(100% - 2rem); max-width: 500px;';
|
|
303
|
+
|
|
304
|
+
const $text = document.createElement('span');
|
|
305
|
+
$text.textContent = text;
|
|
306
|
+
|
|
307
|
+
const $closeBtn = document.createElement('button');
|
|
308
|
+
$closeBtn.type = 'button';
|
|
309
|
+
$closeBtn.className = 'btn-close';
|
|
310
|
+
$closeBtn.setAttribute('data-bs-dismiss', 'alert');
|
|
311
|
+
|
|
312
|
+
$notification.appendChild($text);
|
|
313
|
+
$notification.appendChild($closeBtn);
|
|
314
|
+
|
|
315
|
+
document.body.appendChild($notification);
|
|
316
|
+
|
|
317
|
+
// Auto-remove after timeout (unless timeout is 0)
|
|
318
|
+
if (timeout > 0) {
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
$notification.remove();
|
|
321
|
+
}, timeout);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Get platform (OS)
|
|
326
|
+
getPlatform = () => {
|
|
327
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
328
|
+
const platform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
|
|
329
|
+
|
|
330
|
+
// Check userAgent for mobile platforms (more reliable than platform string)
|
|
331
|
+
if (/iphone|ipad|ipod/.test(ua)) {
|
|
332
|
+
return 'ios';
|
|
333
|
+
}
|
|
334
|
+
if (/android/.test(ua)) {
|
|
335
|
+
return 'android';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check platform string for desktop OS
|
|
339
|
+
if (/win/.test(platform)) {
|
|
340
|
+
return 'windows';
|
|
341
|
+
}
|
|
342
|
+
if (/mac/.test(platform)) {
|
|
343
|
+
return 'mac';
|
|
344
|
+
}
|
|
345
|
+
if (/cros/.test(ua)) {
|
|
346
|
+
return 'chromeos';
|
|
347
|
+
}
|
|
348
|
+
if (/linux/.test(platform)) {
|
|
349
|
+
return 'linux';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return 'unknown';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Get browser name
|
|
356
|
+
getBrowser = () => {
|
|
357
|
+
const ua = navigator.userAgent;
|
|
358
|
+
|
|
359
|
+
// Order matters - check more specific browsers first
|
|
360
|
+
// Edge before Chrome (Edge includes "Chrome" in UA)
|
|
361
|
+
if (/edg/i.test(ua)) {
|
|
362
|
+
return 'edge';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Opera before Chrome (Opera includes "Chrome" in UA)
|
|
366
|
+
if (/opera|opr/i.test(ua)) {
|
|
367
|
+
return 'opera';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Brave before Chrome (Brave includes "Chrome" in UA)
|
|
371
|
+
if (navigator.brave || /brave/i.test(ua)) {
|
|
372
|
+
return 'brave';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Chrome (including Chromium-based browsers)
|
|
376
|
+
if (/chrome|chromium|crios/i.test(ua)) {
|
|
377
|
+
return 'chrome';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Firefox
|
|
381
|
+
if (/firefox|fxios/i.test(ua)) {
|
|
382
|
+
return 'firefox';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Safari last (most browsers include "Safari" in UA)
|
|
386
|
+
if (/safari/i.test(ua)) {
|
|
387
|
+
return 'safari';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Fallback
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Get runtime environment
|
|
395
|
+
getRuntime = () => {
|
|
396
|
+
// Use config runtime if provided
|
|
397
|
+
if (this.manager?.config?.runtime) {
|
|
398
|
+
return this.manager.config.runtime;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Browser extension (Chrome, Edge, Opera, Brave, Firefox, Safari, etc.)
|
|
402
|
+
if (
|
|
403
|
+
(typeof chrome !== 'undefined' && chrome.runtime?.id)
|
|
404
|
+
|| (typeof browser !== 'undefined' && browser.runtime?.id)
|
|
405
|
+
|| (typeof safari !== 'undefined' && safari.extension)
|
|
406
|
+
) {
|
|
407
|
+
return 'browser-extension';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Default: web browser
|
|
411
|
+
return 'web';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check if mobile device
|
|
415
|
+
isMobile = () => {
|
|
416
|
+
try {
|
|
417
|
+
// Try modern API first
|
|
418
|
+
const m = navigator.userAgentData?.mobile;
|
|
419
|
+
if (typeof m !== 'undefined') {
|
|
420
|
+
return m === true;
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
// Silent fail
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Fallback to media query
|
|
427
|
+
try {
|
|
428
|
+
return window.matchMedia('(max-width: 767px)').matches;
|
|
429
|
+
} catch (e) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Get device based on screen width
|
|
435
|
+
getDevice = () => {
|
|
436
|
+
const width = window.innerWidth;
|
|
437
|
+
|
|
438
|
+
// Mobile: < 768px (Bootstrap's md breakpoint)
|
|
439
|
+
if (width < 768) {
|
|
440
|
+
return 'mobile';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Tablet: 768px - 1199px (between md and xl)
|
|
444
|
+
if (width < 1200) {
|
|
445
|
+
return 'tablet';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Desktop: >= 1200px
|
|
449
|
+
return 'desktop';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Get context information
|
|
453
|
+
getContext = () => {
|
|
454
|
+
// Return context information
|
|
455
|
+
return {
|
|
456
|
+
client: {
|
|
457
|
+
language: navigator.language,
|
|
458
|
+
mobile: this.isMobile(),
|
|
459
|
+
device: this.getDevice(),
|
|
460
|
+
platform: this.getPlatform(),
|
|
461
|
+
browser: this.getBrowser(),
|
|
462
|
+
vendor: navigator.vendor,
|
|
463
|
+
runtime: this.getRuntime(),
|
|
464
|
+
userAgent: navigator.userAgent,
|
|
465
|
+
url: window.location.href,
|
|
466
|
+
},
|
|
467
|
+
geolocation: {
|
|
468
|
+
ip: null,
|
|
469
|
+
country: null,
|
|
470
|
+
region: null,
|
|
471
|
+
city: null,
|
|
472
|
+
latitude: null,
|
|
473
|
+
longitude: null,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export default Utilities;
|