@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,319 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
const logger = createLogger('bindings');
|
|
4
|
+
|
|
5
|
+
class Bindings {
|
|
6
|
+
constructor(manager) {
|
|
7
|
+
this.manager = manager;
|
|
8
|
+
this._context = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Update bindings with new data
|
|
12
|
+
update(data = {}) {
|
|
13
|
+
// Merge new data with existing context
|
|
14
|
+
// Whatever keys are provided will overwrite existing values
|
|
15
|
+
this._context = {
|
|
16
|
+
...this._context,
|
|
17
|
+
...data
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Get the top-level keys that were updating
|
|
21
|
+
const updatedKeys = Object.keys(data);
|
|
22
|
+
|
|
23
|
+
this._updateBindings(this._context, updatedKeys);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get current context
|
|
27
|
+
getContext() {
|
|
28
|
+
return this._context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clear all context
|
|
32
|
+
clear() {
|
|
33
|
+
this._context = {};
|
|
34
|
+
this._updateBindings(this._context, null); // null = update all bindings
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Main binding update system
|
|
38
|
+
_updateBindings(context, updatedKeys = null) {
|
|
39
|
+
// Find all elements with data-omega-bind attribute
|
|
40
|
+
const bindElements = document.querySelectorAll('[data-omega-bind]');
|
|
41
|
+
|
|
42
|
+
/* @dev-only:start */
|
|
43
|
+
{
|
|
44
|
+
logger.log('Updating bindings', context, updatedKeys);
|
|
45
|
+
}
|
|
46
|
+
/* @dev-only:end */
|
|
47
|
+
|
|
48
|
+
bindElements.forEach(element => {
|
|
49
|
+
const bindValue = element.getAttribute('data-omega-bind');
|
|
50
|
+
|
|
51
|
+
// Split by comma to support multiple actions
|
|
52
|
+
const bindings = this._parseBindings(bindValue);
|
|
53
|
+
|
|
54
|
+
// Execute each action, track if any were actually processed
|
|
55
|
+
let anyProcessed = false;
|
|
56
|
+
bindings.forEach(({ action, expression }) => {
|
|
57
|
+
if (this._executeAction(element, action, expression, context, updatedKeys)) {
|
|
58
|
+
anyProcessed = true;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Only remove skeleton if at least one binding was actually processed
|
|
63
|
+
if (!anyProcessed) return;
|
|
64
|
+
|
|
65
|
+
// Add bound class to trigger fade out
|
|
66
|
+
element.classList.add('omega-bound');
|
|
67
|
+
|
|
68
|
+
// Remove skeleton class after fade completes
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
element.classList.remove('omega-binding-skeleton');
|
|
71
|
+
}, 300);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse binding string into separate actions
|
|
76
|
+
_parseBindings(bindValue) {
|
|
77
|
+
const bindings = [];
|
|
78
|
+
|
|
79
|
+
// Split by comma, but be smart about it
|
|
80
|
+
// We need to handle cases where commas might be inside expressions
|
|
81
|
+
const parts = bindValue.split(',').map(p => p.trim());
|
|
82
|
+
|
|
83
|
+
parts.forEach(part => {
|
|
84
|
+
let action = '@text'; // Default action
|
|
85
|
+
let expression = part;
|
|
86
|
+
|
|
87
|
+
// Check if it starts with an action keyword
|
|
88
|
+
if (part.startsWith('@')) {
|
|
89
|
+
const spaceIndex = part.indexOf(' ');
|
|
90
|
+
if (spaceIndex > -1) {
|
|
91
|
+
action = part.slice(0, spaceIndex);
|
|
92
|
+
expression = part.slice(spaceIndex + 1).trim();
|
|
93
|
+
} else {
|
|
94
|
+
// No space means it's just an action with no expression (like @hide)
|
|
95
|
+
action = part;
|
|
96
|
+
expression = '';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
bindings.push({ action, expression });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return bindings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Execute a single action on an element
|
|
107
|
+
// Returns true if the action was processed, false if skipped
|
|
108
|
+
_executeAction(element, action, expression, context, updatedKeys = null) {
|
|
109
|
+
switch (action) {
|
|
110
|
+
case '@show':
|
|
111
|
+
// Show element if condition is true (or always if no condition)
|
|
112
|
+
|
|
113
|
+
// Check if this path should be updated
|
|
114
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
|
|
119
|
+
if (shouldShow) {
|
|
120
|
+
element.removeAttribute('hidden');
|
|
121
|
+
} else {
|
|
122
|
+
element.setAttribute('hidden', '');
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
|
|
126
|
+
case '@hide':
|
|
127
|
+
// Hide element if condition is true (or always if no condition)
|
|
128
|
+
|
|
129
|
+
// Check if this path should be updated
|
|
130
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
|
|
135
|
+
if (shouldHide) {
|
|
136
|
+
element.setAttribute('hidden', '');
|
|
137
|
+
} else {
|
|
138
|
+
element.removeAttribute('hidden');
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
|
|
142
|
+
case '@attr':
|
|
143
|
+
// Set attribute value
|
|
144
|
+
// Format: @attr attributeName expression
|
|
145
|
+
const attrParts = expression.split(' ');
|
|
146
|
+
const attrName = attrParts[0];
|
|
147
|
+
const attrExpression = attrParts.slice(1).join(' ');
|
|
148
|
+
|
|
149
|
+
// Check if this path should be updated
|
|
150
|
+
if (!this._shouldUpdatePath(attrExpression, updatedKeys)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const attrValue = this._resolvePath(context, attrExpression) || '';
|
|
155
|
+
|
|
156
|
+
if (attrValue) {
|
|
157
|
+
// Block javascript: protocol on URL attributes to prevent XSS
|
|
158
|
+
const URL_ATTRS = ['href', 'src', 'action', 'formaction'];
|
|
159
|
+
if (URL_ATTRS.includes(attrName.toLowerCase())
|
|
160
|
+
&& /^\s*javascript\s*:/i.test(String(attrValue))) {
|
|
161
|
+
logger.warn(`Blocked javascript: URL in @attr ${attrName}`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
element.setAttribute(attrName, attrValue);
|
|
166
|
+
} else {
|
|
167
|
+
element.removeAttribute(attrName);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
|
|
171
|
+
case '@style':
|
|
172
|
+
// Set CSS custom property or style
|
|
173
|
+
// Format: @style propertyName expression
|
|
174
|
+
const styleParts = expression.split(' ');
|
|
175
|
+
const styleName = styleParts[0];
|
|
176
|
+
const styleExpression = styleParts.slice(1).join(' ');
|
|
177
|
+
|
|
178
|
+
// Check if this path should be updated
|
|
179
|
+
if (!this._shouldUpdatePath(styleExpression, updatedKeys)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const styleValue = this._resolvePath(context, styleExpression);
|
|
184
|
+
|
|
185
|
+
if (styleValue !== null && styleValue !== undefined && styleValue !== '') {
|
|
186
|
+
// If it starts with --, it's a CSS custom property
|
|
187
|
+
if (styleName.startsWith('--')) {
|
|
188
|
+
element.style.setProperty(styleName, styleValue);
|
|
189
|
+
} else {
|
|
190
|
+
// Regular style property
|
|
191
|
+
element.style[styleName] = styleValue;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Remove the style if value is empty
|
|
195
|
+
if (styleName.startsWith('--')) {
|
|
196
|
+
element.style.removeProperty(styleName);
|
|
197
|
+
} else {
|
|
198
|
+
element.style[styleName] = '';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
|
|
203
|
+
case '@value':
|
|
204
|
+
// Set input/textarea value explicitly
|
|
205
|
+
// Check if this path should be updated
|
|
206
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const inputValue = this._resolvePath(context, expression) ?? '';
|
|
211
|
+
element.value = inputValue;
|
|
212
|
+
return true;
|
|
213
|
+
|
|
214
|
+
case '@text':
|
|
215
|
+
default:
|
|
216
|
+
// Set text content (default behavior)
|
|
217
|
+
|
|
218
|
+
// Check if this path should be updated
|
|
219
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const textValue = this._resolvePath(context, expression) ?? '';
|
|
224
|
+
element.textContent = textValue;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if a path should be updated based on updatedKeys
|
|
230
|
+
_shouldUpdatePath(path, updatedKeys) {
|
|
231
|
+
// If no updatedKeys filter, always update
|
|
232
|
+
if (updatedKeys === null || !path) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Strip negation operator if present before extracting root key
|
|
237
|
+
const cleanPath = path.trim().replace(/^!/, '');
|
|
238
|
+
|
|
239
|
+
// Extract the root key from the path
|
|
240
|
+
const rootKey = cleanPath.split('.')[0];
|
|
241
|
+
|
|
242
|
+
// Only update if the root key is in updatedKeys
|
|
243
|
+
return updatedKeys.includes(rootKey);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Resolve nested object path
|
|
247
|
+
_resolvePath(obj, path) {
|
|
248
|
+
if (!obj || !path) return null;
|
|
249
|
+
|
|
250
|
+
return path.split('.').reduce((current, key) => {
|
|
251
|
+
return current?.[key];
|
|
252
|
+
}, obj);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Safely evaluate simple conditions
|
|
256
|
+
_evaluateCondition(condition, context) {
|
|
257
|
+
try {
|
|
258
|
+
// Replace context references with actual values
|
|
259
|
+
// Support: auth.user.field, auth.account.field, simple comparisons
|
|
260
|
+
|
|
261
|
+
// Check for negation operator at the start
|
|
262
|
+
if (condition.trim().startsWith('!')) {
|
|
263
|
+
const expression = condition.trim().slice(1).trim();
|
|
264
|
+
const value = this._resolvePath(context, expression);
|
|
265
|
+
return !value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Parse the condition to extract left side, operator, and right side
|
|
269
|
+
// Longest alternatives first — `>` before `>=` would shadow `>=` forever
|
|
270
|
+
const comparisonMatch = condition.match(/^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
271
|
+
|
|
272
|
+
if (comparisonMatch) {
|
|
273
|
+
const [, leftPath, operator, rightValue] = comparisonMatch;
|
|
274
|
+
|
|
275
|
+
// Get the left side value
|
|
276
|
+
const leftValue = this._resolvePath(context, leftPath.trim());
|
|
277
|
+
|
|
278
|
+
// Parse the right side (could be string, number, boolean)
|
|
279
|
+
let right = rightValue.trim();
|
|
280
|
+
|
|
281
|
+
// Remove quotes if it's a string
|
|
282
|
+
if ((right.startsWith("'") && right.endsWith("'")) ||
|
|
283
|
+
(right.startsWith('"') && right.endsWith('"'))) {
|
|
284
|
+
right = right.slice(1, -1);
|
|
285
|
+
} else if (right === 'true') {
|
|
286
|
+
right = true;
|
|
287
|
+
} else if (right === 'false') {
|
|
288
|
+
right = false;
|
|
289
|
+
} else if (right === 'null') {
|
|
290
|
+
right = null;
|
|
291
|
+
} else if (!isNaN(right)) {
|
|
292
|
+
right = Number(right);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Evaluate based on operator
|
|
296
|
+
switch (operator) {
|
|
297
|
+
case '===': return leftValue === right;
|
|
298
|
+
case '!==': return leftValue !== right;
|
|
299
|
+
case '==': return leftValue == right;
|
|
300
|
+
case '!=': return leftValue != right;
|
|
301
|
+
case '>': return leftValue > right;
|
|
302
|
+
case '<': return leftValue < right;
|
|
303
|
+
case '>=': return leftValue >= right;
|
|
304
|
+
case '<=': return leftValue <= right;
|
|
305
|
+
default: return false;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// Simple truthy check (e.g., "auth.user.emailVerified" or "auth.account")
|
|
309
|
+
const value = this._resolvePath(context, condition.trim());
|
|
310
|
+
return !!value;
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.warn('Failed to evaluate condition:', condition, error);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default Bindings;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
const logger = createLogger('device');
|
|
4
|
+
|
|
5
|
+
// Unit multipliers
|
|
6
|
+
const UNITS = {
|
|
7
|
+
milliseconds: 1,
|
|
8
|
+
seconds: 1000,
|
|
9
|
+
minutes: 1000 * 60,
|
|
10
|
+
hours: 1000 * 60 * 60,
|
|
11
|
+
days: 1000 * 60 * 60 * 24,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Storage key
|
|
15
|
+
const STORAGE_KEY = 'omega_device';
|
|
16
|
+
|
|
17
|
+
// Session timeout (30 minutes of inactivity = new session)
|
|
18
|
+
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
class Device {
|
|
21
|
+
constructor(manager) {
|
|
22
|
+
this.manager = manager;
|
|
23
|
+
this.data = null;
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
this.isNewVersion = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if we're in a browser extension context
|
|
29
|
+
_isExtension() {
|
|
30
|
+
return this.manager.utilities().getRuntime() === 'browser-extension';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get extension storage API
|
|
34
|
+
_getExtensionStorage() {
|
|
35
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
36
|
+
return chrome.storage.local;
|
|
37
|
+
}
|
|
38
|
+
if (typeof browser !== 'undefined' && browser.storage?.local) {
|
|
39
|
+
return browser.storage.local;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Initialize - loads or creates usage data (async for extensions)
|
|
45
|
+
async initialize() {
|
|
46
|
+
// Skip if already initialized
|
|
47
|
+
if (this.initialized) {
|
|
48
|
+
return this.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Load existing data based on runtime
|
|
52
|
+
let existing = null;
|
|
53
|
+
|
|
54
|
+
if (this._isExtension()) {
|
|
55
|
+
existing = await this._loadFromExtensionStorage();
|
|
56
|
+
} else {
|
|
57
|
+
existing = this._loadFromLocalStorage();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const currentVersion = this.manager.config?.version || null;
|
|
62
|
+
|
|
63
|
+
// Stored data is raw JSON.parse output — only a plain object is usable;
|
|
64
|
+
// a primitive or array entry falls through to the first-time payload
|
|
65
|
+
if (existing && typeof existing !== 'object') {
|
|
66
|
+
existing = null;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(existing)) {
|
|
69
|
+
existing = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (existing) {
|
|
73
|
+
this.data = existing;
|
|
74
|
+
|
|
75
|
+
// Check if this is a new session (last activity was more than SESSION_TIMEOUT ago)
|
|
76
|
+
const timeSinceLastActive = now - (this.data.lastActive || 0);
|
|
77
|
+
if (timeSinceLastActive > SESSION_TIMEOUT) {
|
|
78
|
+
// A malformed entry missing `session` (or carrying a non-object
|
|
79
|
+
// there) must not break the whole manager boot
|
|
80
|
+
if (!this.data.session || typeof this.data.session !== 'object') {
|
|
81
|
+
this.data.session = {};
|
|
82
|
+
}
|
|
83
|
+
this.data.session.count = (this.data.session.count || 0) + 1;
|
|
84
|
+
this.data.session.started = now;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update lastActive
|
|
88
|
+
this.data.lastActive = now;
|
|
89
|
+
|
|
90
|
+
// Check for version change
|
|
91
|
+
if (currentVersion && this.data.version?.current !== currentVersion) {
|
|
92
|
+
this.data.version = this.data.version || {};
|
|
93
|
+
this.data.version.previous = this.data.version.current;
|
|
94
|
+
this.data.version.current = currentVersion;
|
|
95
|
+
this.isNewVersion = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this._save();
|
|
99
|
+
} else {
|
|
100
|
+
// First time usage
|
|
101
|
+
this.data = {
|
|
102
|
+
installed: now,
|
|
103
|
+
lastActive: now,
|
|
104
|
+
session: {
|
|
105
|
+
started: now,
|
|
106
|
+
count: 1,
|
|
107
|
+
},
|
|
108
|
+
version: {
|
|
109
|
+
initial: currentVersion,
|
|
110
|
+
current: currentVersion,
|
|
111
|
+
previous: null,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
await this._save();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.initialized = true;
|
|
118
|
+
return this.data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Load from extension storage (async)
|
|
122
|
+
async _loadFromExtensionStorage() {
|
|
123
|
+
const storage = this._getExtensionStorage();
|
|
124
|
+
if (!storage) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await storage.get(STORAGE_KEY);
|
|
130
|
+
return result[STORAGE_KEY] || null;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
logger.warn('Failed to load from extension storage:', e);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Load from localStorage (sync)
|
|
138
|
+
_loadFromLocalStorage() {
|
|
139
|
+
try {
|
|
140
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
141
|
+
return data ? JSON.parse(data) : null;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Save data to storage
|
|
148
|
+
async _save() {
|
|
149
|
+
if (this._isExtension()) {
|
|
150
|
+
await this._saveToExtensionStorage();
|
|
151
|
+
} else {
|
|
152
|
+
this._saveToLocalStorage();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Save to extension storage (async)
|
|
159
|
+
async _saveToExtensionStorage() {
|
|
160
|
+
const storage = this._getExtensionStorage();
|
|
161
|
+
if (!storage) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await storage.set({ [STORAGE_KEY]: this.data });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
logger.warn('Failed to save to extension storage:', e);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Save to localStorage (sync)
|
|
173
|
+
_saveToLocalStorage() {
|
|
174
|
+
try {
|
|
175
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// localStorage not available
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Calculate duration from a timestamp in specified units
|
|
182
|
+
_calculateDuration(timestamp, unit) {
|
|
183
|
+
if (!timestamp) {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Default to milliseconds
|
|
188
|
+
unit = unit || 'milliseconds';
|
|
189
|
+
|
|
190
|
+
// Get multiplier
|
|
191
|
+
const multiplier = UNITS[unit];
|
|
192
|
+
if (!multiplier) {
|
|
193
|
+
throw new Error(`Invalid unit: ${unit}. Valid units: ${Object.keys(UNITS).join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (Date.now() - timestamp) / multiplier;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get total usage duration in specified units (since installed)
|
|
200
|
+
getUsageDuration(unit) {
|
|
201
|
+
return this._calculateDuration(this.data?.installed, unit);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get current session duration in specified units
|
|
205
|
+
getSessionDuration(unit) {
|
|
206
|
+
return this._calculateDuration(this.data?.session?.started, unit);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Get installed date
|
|
210
|
+
getInstalledDate() {
|
|
211
|
+
if (!this.data?.installed) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new Date(this.data.installed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Get session count
|
|
219
|
+
getSessionCount() {
|
|
220
|
+
return this.data?.session?.count || 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Reset usage data (for testing or user request)
|
|
224
|
+
async reset() {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const currentVersion = this.manager.config?.version || null;
|
|
227
|
+
|
|
228
|
+
this.data = {
|
|
229
|
+
installed: now,
|
|
230
|
+
lastActive: now,
|
|
231
|
+
session: {
|
|
232
|
+
started: now,
|
|
233
|
+
count: 1,
|
|
234
|
+
},
|
|
235
|
+
version: {
|
|
236
|
+
initial: currentVersion,
|
|
237
|
+
current: currentVersion,
|
|
238
|
+
previous: null,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
this.isNewVersion = false;
|
|
243
|
+
await this._save();
|
|
244
|
+
return this.data;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get binding-friendly data object for bindings system
|
|
248
|
+
getBindingData() {
|
|
249
|
+
return {
|
|
250
|
+
installed: this.data?.installed || null,
|
|
251
|
+
lastActive: this.data?.lastActive || null,
|
|
252
|
+
session: {
|
|
253
|
+
started: this.data?.session?.started || null,
|
|
254
|
+
count: this.getSessionCount(),
|
|
255
|
+
},
|
|
256
|
+
version: {
|
|
257
|
+
initial: this.data?.version?.initial || null,
|
|
258
|
+
current: this.data?.version?.current || null,
|
|
259
|
+
previous: this.data?.version?.previous || null,
|
|
260
|
+
isNew: this.isNewVersion,
|
|
261
|
+
},
|
|
262
|
+
duration: {
|
|
263
|
+
total: {
|
|
264
|
+
milliseconds: this.getUsageDuration('milliseconds'),
|
|
265
|
+
seconds: this.getUsageDuration('seconds'),
|
|
266
|
+
minutes: this.getUsageDuration('minutes'),
|
|
267
|
+
hours: this.getUsageDuration('hours'),
|
|
268
|
+
days: this.getUsageDuration('days'),
|
|
269
|
+
},
|
|
270
|
+
session: {
|
|
271
|
+
milliseconds: this.getSessionDuration('milliseconds'),
|
|
272
|
+
seconds: this.getSessionDuration('seconds'),
|
|
273
|
+
minutes: this.getSessionDuration('minutes'),
|
|
274
|
+
hours: this.getSessionDuration('hours'),
|
|
275
|
+
days: this.getSessionDuration('days'),
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default Device;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Load external script dynamically
|
|
2
|
+
export function loadScript(options) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
// Handle simple string parameter
|
|
5
|
+
if (typeof options === 'string') {
|
|
6
|
+
options = { src: options };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
src,
|
|
11
|
+
async = true,
|
|
12
|
+
defer = false,
|
|
13
|
+
crossorigin = false,
|
|
14
|
+
integrity = null,
|
|
15
|
+
attributes = {},
|
|
16
|
+
timeout = 60000,
|
|
17
|
+
retries = 0,
|
|
18
|
+
parent = null
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
if (!src) {
|
|
22
|
+
return reject(new Error('Script source is required'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let timeoutId;
|
|
26
|
+
let retryCount = 0;
|
|
27
|
+
|
|
28
|
+
function createAndLoadScript() {
|
|
29
|
+
const script = document.createElement('script');
|
|
30
|
+
script.src = src;
|
|
31
|
+
script.async = async;
|
|
32
|
+
script.defer = defer;
|
|
33
|
+
|
|
34
|
+
if (crossorigin) {
|
|
35
|
+
script.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (integrity) {
|
|
39
|
+
script.integrity = integrity;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add custom attributes
|
|
43
|
+
Object.keys(attributes).forEach(name => {
|
|
44
|
+
script.setAttribute(name, attributes[name]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Set up timeout
|
|
48
|
+
if (timeout > 0) {
|
|
49
|
+
timeoutId = setTimeout(() => {
|
|
50
|
+
script.remove();
|
|
51
|
+
handleError(new Error(`Script load timeout: ${src}`));
|
|
52
|
+
}, timeout);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Event handlers
|
|
56
|
+
script.onload = () => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
resolve({ script, cached: false });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
script.onerror = (error) => {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
script.remove();
|
|
64
|
+
handleError(new Error(`Failed to load script ${src}`, { cause: error }));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Append to document
|
|
68
|
+
const $targetParent = parent || document.head || document.documentElement;
|
|
69
|
+
$targetParent.appendChild(script);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleError(error) {
|
|
73
|
+
if (retryCount < retries) {
|
|
74
|
+
retryCount++;
|
|
75
|
+
setTimeout(createAndLoadScript, 1000 * retryCount);
|
|
76
|
+
} else {
|
|
77
|
+
reject(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createAndLoadScript();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return promise that resolves when DOM is ready
|
|
86
|
+
export function ready() {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
if (document.readyState === 'loading') {
|
|
89
|
+
// Wait for DOM if still loading
|
|
90
|
+
document.addEventListener('DOMContentLoaded', resolve);
|
|
91
|
+
} else {
|
|
92
|
+
// DOM is already ready
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|