web-manager 4.0.36 → 4.0.38

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
@@ -249,7 +249,7 @@ Manager.utilities(); // Utility functions
249
249
  Manager.isDevelopment(); // Check if in development mode
250
250
  Manager.getFunctionsUrl(); // Get Firebase Functions URL
251
251
  Manager.getFunctionsUrl('development'); // Force development URL
252
- Manager.getApiUrl(); // Get API URL (derived from authDomain)
252
+ Manager.getApiUrl(); // Get API URL (derived from firebase authDomain)
253
253
  Manager.isValidRedirectUrl('https://...'); // Validate redirect URL
254
254
 
255
255
  // Firebase instances (after initialization)
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import Storage from './modules/storage.js';
2
- import * as utilities from './modules/utilities.js';
2
+ import Utilities from './modules/utilities.js';
3
3
  import * as domUtils from './modules/dom.js';
4
+ import Analytics from './modules/analytics.js';
4
5
  import Auth from './modules/auth.js';
5
6
  import Bindings from './modules/bindings.js';
6
7
  import Firestore from './modules/firestore.js';
7
8
  import Notifications from './modules/notifications.js';
8
9
  import ServiceWorker from './modules/service-worker.js';
9
10
  import Sentry from './modules/sentry.js';
11
+ import Usage from './modules/usage.js';
10
12
 
11
13
  class Manager {
12
14
  constructor() {
@@ -23,12 +25,15 @@ class Manager {
23
25
 
24
26
  // Initialize modules
25
27
  this._storage = new Storage();
28
+ this._utilities = new Utilities(this);
29
+ this._analytics = new Analytics(this);
26
30
  this._auth = new Auth(this);
27
31
  this._bindings = new Bindings(this);
28
32
  this._firestore = new Firestore(this);
29
33
  this._notifications = new Notifications(this);
30
34
  this._serviceWorker = new ServiceWorker(this);
31
35
  this._sentry = new Sentry(this);
36
+ this._usage = new Usage(this);
32
37
  }
33
38
 
34
39
  // Module getters
@@ -60,13 +65,21 @@ class Manager {
60
65
  return this._sentry;
61
66
  }
62
67
 
68
+ usage() {
69
+ return this._usage;
70
+ }
71
+
72
+ analytics() {
73
+ return this._analytics;
74
+ }
75
+
63
76
  // DOM utilities
64
77
  dom() {
65
78
  return domUtils;
66
79
  }
67
80
 
68
81
  utilities() {
69
- return utilities;
82
+ return this._utilities;
70
83
  }
71
84
 
72
85
  // Initialize the manager
@@ -88,6 +101,16 @@ class Manager {
88
101
  await this._sentry.init(this.config.sentry.config);
89
102
  }
90
103
 
104
+ // Initialize Analytics if tracking ID and secret are provided
105
+ if (this.config.tracking?.['google-analytics'] && this.config.tracking?.['google-analytics-secret']) {
106
+ this._analytics.init({
107
+ id: this.config.tracking['google-analytics'],
108
+ secret: this.config.tracking['google-analytics-secret'],
109
+ });
110
+ } else {
111
+ console.log('[Analytics] Skipped: missing google-analytics ID or secret');
112
+ }
113
+
91
114
  // Initialize service worker if enabled
92
115
  if (this.config.serviceWorker?.enabled) {
93
116
  this._serviceWorker.register({
@@ -111,8 +134,14 @@ class Manager {
111
134
  // Old IE force polyfill
112
135
  // await this._loadPolyfillsIfNeeded();
113
136
 
114
- // Update bindings with config
115
- this.bindings().update({ config: this.config });
137
+ // Initialize usage tracking
138
+ await this._usage.initialize();
139
+
140
+ // Update bindings with config and usage data
141
+ this.bindings().update({
142
+ config: this.config,
143
+ usage: this._usage.getBindingData(),
144
+ });
116
145
 
117
146
  return this;
118
147
  } catch (error) {
@@ -124,6 +153,7 @@ class Manager {
124
153
  _processConfiguration(configuration) {
125
154
  // Default configuration structure
126
155
  const defaults = {
156
+ runtime: null, // Auto-detect if not provided (web, browser-extension, electron, node)
127
157
  environment: 'production',
128
158
  buildTime: Date.now(),
129
159
  brand: {
@@ -266,6 +296,12 @@ class Manager {
266
296
  path: '/service-worker.js'
267
297
  }
268
298
  },
299
+ tracking: {
300
+ 'google-analytics': '',
301
+ 'google-analytics-secret': '',
302
+ 'meta-pixel': '',
303
+ 'tiktok-pixel': '',
304
+ },
269
305
  };
270
306
 
271
307
  // Deep merge configuration with defaults
@@ -284,6 +320,11 @@ class Manager {
284
320
  merged.refreshNewVersion.config.interval = safeEvaluate(merged.refreshNewVersion.config.interval);
285
321
  }
286
322
 
323
+ // Calculate buildTimeISO from buildTime
324
+ if (merged.buildTime) {
325
+ merged.buildTimeISO = new Date(merged.buildTime).toISOString();
326
+ }
327
+
287
328
  // Return merged configuration
288
329
  return merged;
289
330
  }
@@ -318,13 +359,13 @@ class Manager {
318
359
  const $html = document.documentElement;
319
360
 
320
361
  // Set platform (OS) - windows, mac, linux, ios, android, chromeos, unknown
321
- $html.dataset.platform = utilities.getPlatform();
362
+ $html.dataset.platform = this._utilities.getPlatform();
322
363
 
323
- // Set runtime - web, extension, electron, node
324
- $html.dataset.runtime = utilities.getRuntime();
364
+ // Set runtime - web, browser-extension, electron, node
365
+ $html.dataset.runtime = this._utilities.getRuntime();
325
366
 
326
367
  // Set device type - mobile, tablet, desktop
327
- $html.dataset.device = utilities.getDeviceType();
368
+ $html.dataset.device = this._utilities.getDeviceType();
328
369
  }
329
370
 
330
371
  async _initializeFirebase() {
@@ -417,8 +458,9 @@ class Manager {
417
458
  return 'http://localhost:5002';
418
459
  }
419
460
 
420
- const authDomain = this.config.firebase.app.config.authDomain;
421
- const baseUrl = url || (authDomain ? `https://${authDomain}` : window.location.origin);
461
+ const apiDomain = this.config.firebase.app.config.authDomain; // Has to be this since some projects like Clockii use ITW Universal Auth
462
+ // const apiDomain = this.config.brand.url;
463
+ const baseUrl = url || (apiDomain ? `https://${apiDomain}` : window.location.origin);
422
464
  const urlObj = new URL(baseUrl);
423
465
  const hostnameParts = urlObj.hostname.split('.');
424
466
 
@@ -0,0 +1,250 @@
1
+ // Dev mode credentials by runtime
2
+ const DEV_CREDENTIALS = {
3
+ 'browser-extension': {
4
+ id: 'G-5NWE9SPEEM',
5
+ secret: '33E5W1cCQGKPK4lyMMOWOQ',
6
+ },
7
+ 'electron': {
8
+ id: 'G-WMNERKK9J2',
9
+ secret: 'UeKzq8UvS3GD5D2aHcuZgQ',
10
+ },
11
+ };
12
+
13
+ // Supported runtimes for analytics
14
+ const SUPPORTED_RUNTIMES = ['browser-extension', 'electron'];
15
+
16
+ class Analytics {
17
+ constructor(manager) {
18
+ this.manager = manager;
19
+ this.initialized = false;
20
+ this.devMode = false;
21
+ this.runtime = null;
22
+ this.config = null;
23
+ this.measurementId = null;
24
+ this.secret = null;
25
+ this.clientId = null;
26
+ }
27
+
28
+ // Check if runtime is supported
29
+ _isSupported() {
30
+ return SUPPORTED_RUNTIMES.includes(this.runtime);
31
+ }
32
+
33
+ // Initialize analytics
34
+ init(config = {}) {
35
+ // Store config
36
+ this.config = config;
37
+
38
+ // Get runtime
39
+ this.runtime = this.manager.utilities().getRuntime();
40
+
41
+ // TODO: Add web runtime support
42
+ if (!this._isSupported()) {
43
+ console.log(`[Analytics] Runtime "${this.runtime}" not supported yet, skipping`);
44
+ return;
45
+ }
46
+
47
+ // Skip if already initialized
48
+ if (this.initialized) {
49
+ console.log('[Analytics] Already initialized');
50
+ return;
51
+ }
52
+
53
+ // Check for development mode
54
+ this.devMode = this.manager.isDevelopment();
55
+
56
+ // Get measurement ID and secret (use dev credentials in dev mode)
57
+ if (this.devMode) {
58
+ const devCreds = DEV_CREDENTIALS[this.runtime];
59
+ if (devCreds) {
60
+ this.measurementId = devCreds.id;
61
+ this.secret = devCreds.secret;
62
+ console.log(`[Analytics] Dev mode: using ${this.runtime} dev credentials`);
63
+ } else {
64
+ // No dev credentials for this runtime, use provided config
65
+ this.measurementId = config.measurementId || config.id;
66
+ this.secret = config.secret;
67
+ }
68
+ } else {
69
+ this.measurementId = config.measurementId || config.id;
70
+ this.secret = config.secret;
71
+ }
72
+
73
+ // Skip if no measurement ID
74
+ if (!this.measurementId) {
75
+ console.log('[Analytics] No measurement ID provided, skipping initialization');
76
+ return;
77
+ }
78
+
79
+ // Generate or retrieve client ID
80
+ this.clientId = this._getClientId();
81
+
82
+ // Log initialization
83
+ console.log(`[Analytics] Initializing with measurement ID: ${this.measurementId}${this.devMode ? ' (dev mode)' : ''} [${this.runtime}]`);
84
+
85
+ // Mark as initialized
86
+ this.initialized = true;
87
+
88
+ // Send initial pageview
89
+ this.event('page_view');
90
+ }
91
+
92
+ // Get or generate client ID
93
+ _getClientId() {
94
+ const storageKey = '_ga_client_id';
95
+
96
+ // Try to get existing client ID
97
+ let clientId = null;
98
+ try {
99
+ clientId = localStorage.getItem(storageKey);
100
+ } catch (e) {
101
+ // localStorage not available
102
+ }
103
+
104
+ // Generate new client ID if needed
105
+ if (!clientId) {
106
+ clientId = `${Math.random().toString(36).substring(2)}.${Date.now()}`;
107
+ try {
108
+ localStorage.setItem(storageKey, clientId);
109
+ } catch (e) {
110
+ // localStorage not available
111
+ }
112
+ }
113
+
114
+ return clientId;
115
+ }
116
+
117
+ // Get page data to include with all events
118
+ _getPageData() {
119
+ return {
120
+ page_path: window.location.pathname,
121
+ page_title: document.title,
122
+ page_location: window.location.href,
123
+ };
124
+ }
125
+
126
+ // Track an event
127
+ event(eventName, params = {}) {
128
+ // TODO: Add web runtime support
129
+ if (!this._isSupported()) {
130
+ return;
131
+ }
132
+
133
+ if (!this.initialized) {
134
+ return;
135
+ }
136
+
137
+ // Merge page data with provided params
138
+ const eventParams = {
139
+ ...this._getPageData(),
140
+ ...params,
141
+ };
142
+
143
+ // Log event
144
+ console.log(`[Analytics] Event: ${eventName}${this.devMode ? ' (dev mode)' : ''}`, eventParams);
145
+
146
+ // Send via Measurement Protocol (fetch)
147
+ this._sendViaFetch(eventName, eventParams);
148
+ }
149
+
150
+ // Send event via Measurement Protocol (fetch)
151
+ _sendViaFetch(eventName, params = {}) {
152
+ // Measurement Protocol requires api_secret
153
+ if (!this.secret) {
154
+ console.warn('[Analytics] No API secret provided, cannot send via Measurement Protocol');
155
+ return;
156
+ }
157
+
158
+ const url = `https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&api_secret=${this.secret}`;
159
+
160
+ const payload = {
161
+ client_id: this.clientId,
162
+ events: [{
163
+ name: eventName,
164
+ params: {
165
+ ...params,
166
+ engagement_time_msec: 100,
167
+ session_id: this._getSessionId(),
168
+ },
169
+ }],
170
+ };
171
+
172
+ // Send via fetch (fire and forget)
173
+ fetch(url, {
174
+ method: 'POST',
175
+ body: JSON.stringify(payload),
176
+ }).catch((err) => {
177
+ console.warn('[Analytics] Failed to send event:', err);
178
+ });
179
+ }
180
+
181
+ // Get or generate session ID
182
+ _getSessionId() {
183
+ const storageKey = '_ga_session_id';
184
+ const sessionTimeout = 30 * 60 * 1000; // 30 minutes
185
+
186
+ let sessionData = null;
187
+ try {
188
+ sessionData = JSON.parse(sessionStorage.getItem(storageKey) || 'null');
189
+ } catch (e) {
190
+ // sessionStorage not available
191
+ }
192
+
193
+ const now = Date.now();
194
+
195
+ // Check if session is still valid
196
+ if (sessionData && (now - sessionData.lastActive) < sessionTimeout) {
197
+ sessionData.lastActive = now;
198
+ try {
199
+ sessionStorage.setItem(storageKey, JSON.stringify(sessionData));
200
+ } catch (e) {
201
+ // sessionStorage not available
202
+ }
203
+ return sessionData.id;
204
+ }
205
+
206
+ // Create new session
207
+ const newSession = {
208
+ id: `${Date.now()}`,
209
+ lastActive: now,
210
+ };
211
+
212
+ try {
213
+ sessionStorage.setItem(storageKey, JSON.stringify(newSession));
214
+ } catch (e) {
215
+ // sessionStorage not available
216
+ }
217
+
218
+ return newSession.id;
219
+ }
220
+
221
+ // Set user properties
222
+ setUserProperties(properties = {}) {
223
+ // TODO: Add web runtime support
224
+ if (!this._isSupported()) {
225
+ return;
226
+ }
227
+
228
+ if (!this.initialized) {
229
+ return;
230
+ }
231
+
232
+ // TODO: Implement for Measurement Protocol
233
+ }
234
+
235
+ // Set user ID
236
+ setUserId(userId) {
237
+ // TODO: Add web runtime support
238
+ if (!this._isSupported()) {
239
+ return;
240
+ }
241
+
242
+ if (!this.initialized) {
243
+ return;
244
+ }
245
+
246
+ // TODO: Implement for Measurement Protocol
247
+ }
248
+ }
249
+
250
+ export default Analytics;
@@ -0,0 +1,264 @@
1
+ // Unit multipliers
2
+ const UNITS = {
3
+ milliseconds: 1,
4
+ seconds: 1000,
5
+ minutes: 1000 * 60,
6
+ hours: 1000 * 60 * 60,
7
+ days: 1000 * 60 * 60 * 24,
8
+ };
9
+
10
+ // Storage key
11
+ const STORAGE_KEY = 'wm_usage';
12
+
13
+ // Session timeout (30 minutes of inactivity = new session)
14
+ const SESSION_TIMEOUT = 30 * 60 * 1000;
15
+
16
+ class Usage {
17
+ constructor(manager) {
18
+ this.manager = manager;
19
+ this.data = null;
20
+ this.initialized = false;
21
+ this.isNewVersion = false;
22
+ }
23
+
24
+ // Check if we're in a browser extension context
25
+ _isExtension() {
26
+ return this.manager.utilities().getRuntime() === 'browser-extension';
27
+ }
28
+
29
+ // Get extension storage API
30
+ _getExtensionStorage() {
31
+ if (typeof chrome !== 'undefined' && chrome.storage?.local) {
32
+ return chrome.storage.local;
33
+ }
34
+ if (typeof browser !== 'undefined' && browser.storage?.local) {
35
+ return browser.storage.local;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ // Initialize - loads or creates usage data (async for extensions)
41
+ async initialize() {
42
+ // Skip if already initialized
43
+ if (this.initialized) {
44
+ return this.data;
45
+ }
46
+
47
+ // Load existing data based on runtime
48
+ let existing = null;
49
+
50
+ if (this._isExtension()) {
51
+ existing = await this._loadFromExtensionStorage();
52
+ } else {
53
+ existing = this._loadFromLocalStorage();
54
+ }
55
+
56
+ const now = Date.now();
57
+ const currentVersion = this.manager.config?.version || null;
58
+
59
+ if (existing) {
60
+ this.data = existing;
61
+
62
+ // Check if this is a new session (last activity was more than SESSION_TIMEOUT ago)
63
+ const timeSinceLastActive = now - (this.data.lastActive || 0);
64
+ if (timeSinceLastActive > SESSION_TIMEOUT) {
65
+ this.data.session.count = (this.data.session?.count || 0) + 1;
66
+ this.data.session.started = now;
67
+ }
68
+
69
+ // Update lastActive
70
+ this.data.lastActive = now;
71
+
72
+ // Check for version change
73
+ if (currentVersion && this.data.version?.current !== currentVersion) {
74
+ this.data.version = this.data.version || {};
75
+ this.data.version.previous = this.data.version.current;
76
+ this.data.version.current = currentVersion;
77
+ this.isNewVersion = true;
78
+ }
79
+
80
+ await this._save();
81
+ } else {
82
+ // First time usage
83
+ this.data = {
84
+ installed: now,
85
+ lastActive: now,
86
+ session: {
87
+ started: now,
88
+ count: 1,
89
+ },
90
+ version: {
91
+ initial: currentVersion,
92
+ current: currentVersion,
93
+ previous: null,
94
+ },
95
+ };
96
+ await this._save();
97
+ }
98
+
99
+ this.initialized = true;
100
+ return this.data;
101
+ }
102
+
103
+ // Load from extension storage (async)
104
+ async _loadFromExtensionStorage() {
105
+ const storage = this._getExtensionStorage();
106
+ if (!storage) {
107
+ return null;
108
+ }
109
+
110
+ try {
111
+ const result = await storage.get(STORAGE_KEY);
112
+ return result[STORAGE_KEY] || null;
113
+ } catch (e) {
114
+ console.warn('[Usage] Failed to load from extension storage:', e);
115
+ return null;
116
+ }
117
+ }
118
+
119
+ // Load from localStorage (sync)
120
+ _loadFromLocalStorage() {
121
+ try {
122
+ const data = localStorage.getItem(STORAGE_KEY);
123
+ return data ? JSON.parse(data) : null;
124
+ } catch (e) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ // Save data to storage
130
+ async _save() {
131
+ if (this._isExtension()) {
132
+ await this._saveToExtensionStorage();
133
+ } else {
134
+ this._saveToLocalStorage();
135
+ }
136
+
137
+ return this.data;
138
+ }
139
+
140
+ // Save to extension storage (async)
141
+ async _saveToExtensionStorage() {
142
+ const storage = this._getExtensionStorage();
143
+ if (!storage) {
144
+ return;
145
+ }
146
+
147
+ try {
148
+ await storage.set({ [STORAGE_KEY]: this.data });
149
+ } catch (e) {
150
+ console.warn('[Usage] Failed to save to extension storage:', e);
151
+ }
152
+ }
153
+
154
+ // Save to localStorage (sync)
155
+ _saveToLocalStorage() {
156
+ try {
157
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
158
+ } catch (e) {
159
+ // localStorage not available
160
+ }
161
+ }
162
+
163
+ // Calculate duration from a timestamp in specified units
164
+ _calculateDuration(timestamp, unit) {
165
+ if (!timestamp) {
166
+ return 0;
167
+ }
168
+
169
+ // Default to milliseconds
170
+ unit = unit || 'milliseconds';
171
+
172
+ // Get multiplier
173
+ const multiplier = UNITS[unit];
174
+ if (!multiplier) {
175
+ throw new Error(`Invalid unit: ${unit}. Valid units: ${Object.keys(UNITS).join(', ')}`);
176
+ }
177
+
178
+ return (Date.now() - timestamp) / multiplier;
179
+ }
180
+
181
+ // Get total usage duration in specified units (since installed)
182
+ getUsageDuration(unit) {
183
+ return this._calculateDuration(this.data?.installed, unit);
184
+ }
185
+
186
+ // Get current session duration in specified units
187
+ getSessionDuration(unit) {
188
+ return this._calculateDuration(this.data?.session?.started, unit);
189
+ }
190
+
191
+ // Get installed date
192
+ getInstalledDate() {
193
+ if (!this.data?.installed) {
194
+ return null;
195
+ }
196
+
197
+ return new Date(this.data.installed);
198
+ }
199
+
200
+ // Get session count
201
+ getSessionCount() {
202
+ return this.data?.session?.count || 0;
203
+ }
204
+
205
+ // Reset usage data (for testing or user request)
206
+ async reset() {
207
+ const now = Date.now();
208
+ const currentVersion = this.manager.config?.version || null;
209
+
210
+ this.data = {
211
+ installed: now,
212
+ lastActive: now,
213
+ session: {
214
+ started: now,
215
+ count: 1,
216
+ },
217
+ version: {
218
+ initial: currentVersion,
219
+ current: currentVersion,
220
+ previous: null,
221
+ },
222
+ };
223
+
224
+ this.isNewVersion = false;
225
+ await this._save();
226
+ return this.data;
227
+ }
228
+
229
+ // Get binding-friendly data object for bindings system
230
+ getBindingData() {
231
+ return {
232
+ installed: this.data?.installed || null,
233
+ lastActive: this.data?.lastActive || null,
234
+ session: {
235
+ started: this.data?.session?.started || null,
236
+ count: this.getSessionCount(),
237
+ },
238
+ version: {
239
+ initial: this.data?.version?.initial || null,
240
+ current: this.data?.version?.current || null,
241
+ previous: this.data?.version?.previous || null,
242
+ isNew: this.isNewVersion,
243
+ },
244
+ duration: {
245
+ total: {
246
+ milliseconds: this.getUsageDuration('milliseconds'),
247
+ seconds: this.getUsageDuration('seconds'),
248
+ minutes: this.getUsageDuration('minutes'),
249
+ hours: this.getUsageDuration('hours'),
250
+ days: this.getUsageDuration('days'),
251
+ },
252
+ session: {
253
+ milliseconds: this.getSessionDuration('milliseconds'),
254
+ seconds: this.getSessionDuration('seconds'),
255
+ minutes: this.getSessionDuration('minutes'),
256
+ hours: this.getSessionDuration('hours'),
257
+ days: this.getSessionDuration('days'),
258
+ },
259
+ },
260
+ };
261
+ }
262
+ }
263
+
264
+ export default Usage;
@@ -1,222 +1,209 @@
1
- // Copy text to clipboard
2
- export function clipboardCopy(input) {
3
- // Get the text from the input
4
- const text = input && input.nodeType
5
- ? input.value || input.innerText || input.innerHTML
6
- : input;
7
-
8
- // Try to use the modern clipboard API
9
- if (navigator.clipboard && navigator.clipboard.writeText) {
10
- return navigator.clipboard.writeText(text).catch(() => {
1
+ class Utilities {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ }
5
+
6
+ // Copy text to clipboard
7
+ clipboardCopy(input) {
8
+ // Get the text from the input
9
+ const text = input && input.nodeType
10
+ ? input.value || input.innerText || input.innerHTML
11
+ : input;
12
+
13
+ // Try to use the modern clipboard API
14
+ if (navigator.clipboard && navigator.clipboard.writeText) {
15
+ return navigator.clipboard.writeText(text).catch(() => {
16
+ fallbackCopy(text);
17
+ });
18
+ } else {
11
19
  fallbackCopy(text);
12
- });
13
- } else {
14
- fallbackCopy(text);
15
- }
20
+ }
16
21
 
17
- function fallbackCopy(text) {
18
- const el = document.createElement('textarea');
19
- el.setAttribute('style', 'width:1px;border:0;opacity:0;');
20
- el.value = text;
21
- document.body.appendChild(el);
22
- el.select();
22
+ function fallbackCopy(text) {
23
+ const el = document.createElement('textarea');
24
+ el.setAttribute('style', 'width:1px;border:0;opacity:0;');
25
+ el.value = text;
26
+ document.body.appendChild(el);
27
+ el.select();
23
28
 
24
- try {
25
- document.execCommand('copy');
26
- } catch (e) {
27
- console.error('Failed to copy to clipboard');
28
- }
29
+ try {
30
+ document.execCommand('copy');
31
+ } catch (e) {
32
+ console.error('Failed to copy to clipboard');
33
+ }
29
34
 
30
- document.body.removeChild(el);
35
+ document.body.removeChild(el);
36
+ }
31
37
  }
32
- }
33
38
 
34
- // Escape HTML to prevent XSS
35
- let shadowElement;
36
- export function escapeHTML(str) {
37
- if (typeof str !== 'string') {
38
- return '';
39
- }
39
+ // Escape HTML to prevent XSS
40
+ escapeHTML(str) {
41
+ if (typeof str !== 'string') {
42
+ return '';
43
+ }
40
44
 
41
- shadowElement = shadowElement || document.createElement('p');
42
- shadowElement.innerHTML = '';
45
+ this._shadowElement = this._shadowElement || document.createElement('p');
46
+ this._shadowElement.innerHTML = '';
43
47
 
44
- // This automatically escapes HTML entities like <, >, &, etc.
45
- shadowElement.appendChild(document.createTextNode(str));
48
+ // This automatically escapes HTML entities like <, >, &, etc.
49
+ this._shadowElement.appendChild(document.createTextNode(str));
46
50
 
47
- // This is needed to escape quotes to prevent attribute injection
48
- return shadowElement.innerHTML.replace(/["']/g, (m) => {
49
- switch (m) {
50
- case '"':
51
- return '&quot;';
52
- default:
53
- return '&#039;';
54
- }
55
- });
56
- }
51
+ // This is needed to escape quotes to prevent attribute injection
52
+ return this._shadowElement.innerHTML.replace(/["']/g, (m) => {
53
+ switch (m) {
54
+ case '"':
55
+ return '&quot;';
56
+ default:
57
+ return '&#039;';
58
+ }
59
+ });
60
+ }
57
61
 
58
- // Show notification
59
- export function showNotification(message, options = {}) {
60
- // Handle different input types
61
- let text = message;
62
- let type = options.type || 'info';
62
+ // Show notification
63
+ showNotification(message, options = {}) {
64
+ // Handle different input types
65
+ let text = message;
66
+ let type = options.type || 'info';
63
67
 
64
- // If message is an Error object, extract message and default to danger
65
- if (message instanceof Error) {
66
- text = message.message;
67
- type = options.type || 'danger';
68
- }
68
+ // If message is an Error object, extract message and default to danger
69
+ if (message instanceof Error) {
70
+ text = message.message;
71
+ type = options.type || 'danger';
72
+ }
69
73
 
70
- // Handle string as second parameter for backwards compatibility
71
- if (typeof options === 'string') {
72
- options = { type: options };
73
- type = options.type;
74
- }
74
+ // Handle string as second parameter for backwards compatibility
75
+ if (typeof options === 'string') {
76
+ options = { type: options };
77
+ type = options.type;
78
+ }
75
79
 
76
- // Extract options
77
- const timeout = options.timeout !== undefined ? options.timeout : 5000;
80
+ // Extract options
81
+ const timeout = options.timeout !== undefined ? options.timeout : 5000;
78
82
 
79
- const $notification = document.createElement('div');
80
- $notification.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-5`;
81
- $notification.style.zIndex = '9999';
82
- $notification.innerHTML = `
83
- ${text}
84
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
85
- `;
83
+ const $notification = document.createElement('div');
84
+ $notification.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-5`;
85
+ $notification.style.zIndex = '9999';
86
+ $notification.innerHTML = `
87
+ ${text}
88
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
89
+ `;
86
90
 
87
- document.body.appendChild($notification);
91
+ document.body.appendChild($notification);
88
92
 
89
- // Auto-remove after timeout (unless timeout is 0)
90
- if (timeout > 0) {
91
- setTimeout(() => {
92
- $notification.remove();
93
- }, timeout);
93
+ // Auto-remove after timeout (unless timeout is 0)
94
+ if (timeout > 0) {
95
+ setTimeout(() => {
96
+ $notification.remove();
97
+ }, timeout);
98
+ }
94
99
  }
95
- }
96
100
 
97
- // Get platform (OS)
98
- export function getPlatform() {
99
- const ua = navigator.userAgent.toLowerCase();
100
- const platform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
101
+ // Get platform (OS)
102
+ getPlatform() {
103
+ const ua = navigator.userAgent.toLowerCase();
104
+ const platform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
101
105
 
102
- // Check userAgent for mobile platforms (more reliable than platform string)
103
- if (/iphone|ipad|ipod/.test(ua)) {
104
- return 'ios';
105
- }
106
- if (/android/.test(ua)) {
107
- return 'android';
108
- }
106
+ // Check userAgent for mobile platforms (more reliable than platform string)
107
+ if (/iphone|ipad|ipod/.test(ua)) {
108
+ return 'ios';
109
+ }
110
+ if (/android/.test(ua)) {
111
+ return 'android';
112
+ }
109
113
 
110
- // Check platform string for desktop OS
111
- if (/win/.test(platform)) {
112
- return 'windows';
113
- }
114
- if (/mac/.test(platform)) {
115
- return 'mac';
116
- }
117
- if (/cros/.test(ua)) {
118
- return 'chromeos';
119
- }
120
- if (/linux/.test(platform)) {
121
- return 'linux';
114
+ // Check platform string for desktop OS
115
+ if (/win/.test(platform)) {
116
+ return 'windows';
117
+ }
118
+ if (/mac/.test(platform)) {
119
+ return 'mac';
120
+ }
121
+ if (/cros/.test(ua)) {
122
+ return 'chromeos';
123
+ }
124
+ if (/linux/.test(platform)) {
125
+ return 'linux';
126
+ }
127
+
128
+ return 'unknown';
122
129
  }
123
130
 
124
- return 'unknown';
125
- }
131
+ // Get runtime environment
132
+ getRuntime() {
133
+ // Use config runtime if provided
134
+ if (this.manager?.config?.runtime) {
135
+ return this.manager.config.runtime;
136
+ }
126
137
 
127
- // Get runtime environment
128
- export function getRuntime() {
129
- // Browser extension (Chrome, Edge, Opera, Brave, Firefox, Safari, etc.)
130
- if (
131
- (typeof chrome !== 'undefined' && chrome.runtime?.id)
132
- || (typeof browser !== 'undefined' && browser.runtime?.id)
133
- || (typeof safari !== 'undefined' && safari.extension)
134
- ) {
135
- return 'browser-extension';
138
+ // Browser extension (Chrome, Edge, Opera, Brave, Firefox, Safari, etc.)
139
+ if (
140
+ (typeof chrome !== 'undefined' && chrome.runtime?.id)
141
+ || (typeof browser !== 'undefined' && browser.runtime?.id)
142
+ || (typeof safari !== 'undefined' && safari.extension)
143
+ ) {
144
+ return 'browser-extension';
145
+ }
146
+
147
+ // Default: web browser
148
+ return 'web';
136
149
  }
137
150
 
138
- // // Electron (main process, or renderer with nodeIntegration, or via userAgent)
139
- // if (
140
- // (typeof process !== 'undefined' && process.versions?.electron) ||
141
- // (typeof navigator !== 'undefined' && /electron/i.test(navigator.userAgent))
142
- // ) {
143
- // return 'electron';
144
- // }
145
-
146
- // // Node.js (no window)
147
- // if (typeof window === 'undefined') {
148
- // return 'node';
149
- // }
150
-
151
- // Default: web browser
152
- return 'web';
153
- }
151
+ // Check if mobile device
152
+ isMobile() {
153
+ try {
154
+ // Try modern API first
155
+ const m = navigator.userAgentData?.mobile;
156
+ if (typeof m !== 'undefined') {
157
+ return m === true;
158
+ }
159
+ } catch (e) {
160
+ // Silent fail
161
+ }
154
162
 
155
- // Check if mobile device
156
- export function isMobile() {
157
- try {
158
- // Try modern API first
159
- const m = navigator.userAgentData?.mobile;
160
- if (typeof m !== 'undefined') {
161
- return m === true;
163
+ // Fallback to media query
164
+ try {
165
+ return window.matchMedia('(max-width: 767px)').matches;
166
+ } catch (e) {
167
+ return false;
162
168
  }
163
- } catch (e) {
164
- // Silent fail
165
169
  }
166
170
 
167
- // Fallback to media query
168
- try {
169
- return window.matchMedia('(max-width: 767px)').matches;
170
- } catch (e) {
171
- return false;
172
- }
173
- }
171
+ // Get device type based on screen width
172
+ getDeviceType() {
173
+ const width = window.innerWidth;
174
174
 
175
- // Get device type based on screen width
176
- export function getDeviceType() {
177
- const width = window.innerWidth;
175
+ // Mobile: < 768px (Bootstrap's md breakpoint)
176
+ if (width < 768) {
177
+ return 'mobile';
178
+ }
178
179
 
179
- // Mobile: < 768px (Bootstrap's md breakpoint)
180
- if (width < 768) {
181
- return 'mobile';
182
- }
180
+ // Tablet: 768px - 1199px (between md and xl)
181
+ if (width < 1200) {
182
+ return 'tablet';
183
+ }
183
184
 
184
- // Tablet: 768px - 1199px (between md and xl)
185
- if (width < 1200) {
186
- return 'tablet';
185
+ // Desktop: >= 1200px
186
+ return 'desktop';
187
+ }
188
+
189
+ // Get context information
190
+ getContext() {
191
+ // Return context information
192
+ return {
193
+ client: {
194
+ language: navigator.language,
195
+ mobile: this.isMobile(),
196
+ deviceType: this.getDeviceType(),
197
+ platform: this.getPlatform(),
198
+ runtime: this.getRuntime(),
199
+ userAgent: navigator.userAgent,
200
+ url: window.location.href,
201
+ },
202
+ browser: {
203
+ vendor: navigator.vendor,
204
+ },
205
+ };
187
206
  }
188
-
189
- // Desktop: >= 1200px
190
- return 'desktop';
191
207
  }
192
208
 
193
- // Get context information
194
- export function getContext() {
195
- // Return context information
196
- return {
197
- client: {
198
- language: navigator.language,
199
- mobile: isMobile(),
200
- deviceType: getDeviceType(),
201
- platform: getPlatform(),
202
- runtime: getRuntime(),
203
- userAgent: navigator.userAgent,
204
- url: window.location.href,
205
- },
206
- browser: {
207
- vendor: navigator.vendor,
208
- },
209
- // screen: {
210
- // width: window.screen?.width,
211
- // height: window.screen?.height,
212
- // availWidth: window.screen?.availWidth,
213
- // availHeight: window.screen?.availHeight,
214
- // colorDepth: window.screen?.colorDepth,
215
- // pixelRatio: window.devicePixelRatio || 1,
216
- // },
217
- // viewport: {
218
- // width: window.innerWidth,
219
- // height: window.innerHeight,
220
- // },
221
- };
222
- }
209
+ export default Utilities;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-manager",
3
- "version": "4.0.36",
3
+ "version": "4.0.38",
4
4
  "description": "Easily access important variables such as the query string, current domain, and current page in a single object.",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.js",