web-manager 4.3.2 → 4.3.4
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/CLAUDE.md +3 -0
- package/dist/index.js +11 -1
- package/docs/cdp-debugging.md +29 -0
- package/package.json +7 -1
- package/src/index.js +712 -0
- package/src/modules/analytics.js +250 -0
- package/src/modules/auth.js +420 -0
- package/src/modules/bindings.js +314 -0
- package/src/modules/dom.js +96 -0
- package/src/modules/firestore.js +306 -0
- package/src/modules/notifications.js +391 -0
- package/src/modules/sentry.js +199 -0
- package/src/modules/service-worker.js +196 -0
- package/src/modules/storage.js +133 -0
- package/src/modules/usage.js +264 -0
- package/src/modules/utilities.js +310 -0
- package/CHANGELOG.md +0 -242
- package/TODO.md +0 -59
- package/_legacy/helpers/auth-pages.js +0 -24
- package/_legacy/index.js +0 -1854
- package/_legacy/lib/account.js +0 -666
- package/_legacy/lib/debug.js +0 -56
- package/_legacy/lib/dom.js +0 -351
- package/_legacy/lib/require.js +0 -5
- package/_legacy/lib/storage.js +0 -78
- package/_legacy/lib/utilities.js +0 -180
- package/_legacy/service-worker copy.js +0 -347
- package/_legacy/test/test.js +0 -158
package/src/index.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import Storage from './modules/storage.js';
|
|
2
|
+
import Utilities from './modules/utilities.js';
|
|
3
|
+
import * as domUtils from './modules/dom.js';
|
|
4
|
+
import Analytics from './modules/analytics.js';
|
|
5
|
+
import Auth from './modules/auth.js';
|
|
6
|
+
import Bindings from './modules/bindings.js';
|
|
7
|
+
import Firestore from './modules/firestore.js';
|
|
8
|
+
import Notifications from './modules/notifications.js';
|
|
9
|
+
import ServiceWorker from './modules/service-worker.js';
|
|
10
|
+
import Sentry from './modules/sentry.js';
|
|
11
|
+
import Usage from './modules/usage.js';
|
|
12
|
+
|
|
13
|
+
class Manager {
|
|
14
|
+
constructor() {
|
|
15
|
+
// Configuration from init()
|
|
16
|
+
this.config = {};
|
|
17
|
+
|
|
18
|
+
// Runtime state
|
|
19
|
+
this.state = {
|
|
20
|
+
serviceWorker: null
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Auth settler: resolves when Firebase auth first determines user state
|
|
24
|
+
this._firebaseAuthInitialized = false;
|
|
25
|
+
this._authReadyResolve = null;
|
|
26
|
+
this._authReady = new Promise((resolve) => {
|
|
27
|
+
this._authReadyResolve = resolve;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Initialize modules
|
|
31
|
+
this._storage = new Storage();
|
|
32
|
+
this._utilities = new Utilities(this);
|
|
33
|
+
this._analytics = new Analytics(this);
|
|
34
|
+
this._auth = new Auth(this);
|
|
35
|
+
this._bindings = new Bindings(this);
|
|
36
|
+
this._firestore = new Firestore(this);
|
|
37
|
+
this._notifications = new Notifications(this);
|
|
38
|
+
this._serviceWorker = new ServiceWorker(this);
|
|
39
|
+
this._sentry = new Sentry(this);
|
|
40
|
+
this._usage = new Usage(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Module getters
|
|
44
|
+
storage() {
|
|
45
|
+
return this._storage;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
auth() {
|
|
49
|
+
return this._auth;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
bindings() {
|
|
53
|
+
return this._bindings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
firestore() {
|
|
57
|
+
return this._firestore;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
notifications() {
|
|
61
|
+
return this._notifications;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
serviceWorker() {
|
|
65
|
+
return this._serviceWorker;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
sentry() {
|
|
69
|
+
return this._sentry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
usage() {
|
|
73
|
+
return this._usage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
analytics() {
|
|
77
|
+
return this._analytics;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// DOM utilities
|
|
81
|
+
dom() {
|
|
82
|
+
return domUtils;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
utilities() {
|
|
86
|
+
return this._utilities;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Initialize the manager
|
|
90
|
+
async initialize(configuration) {
|
|
91
|
+
try {
|
|
92
|
+
// Store configuration as-is
|
|
93
|
+
this.config = this._processConfiguration(configuration);
|
|
94
|
+
|
|
95
|
+
// Set platform and runtime on HTML element
|
|
96
|
+
this._setHtmlDataAttributes();
|
|
97
|
+
|
|
98
|
+
// Initialize Firebase if a config blob is present (presence-driven — matches BEM
|
|
99
|
+
// convention). Reads flat `firebaseConfig` (BEM/BXM/EM canonical shape) and falls
|
|
100
|
+
// back to nested `firebase.app.config` (UJM's current `_config.yml` shape).
|
|
101
|
+
// Once UJM migrates to the flat shape this fallback can be dropped.
|
|
102
|
+
if (this._resolveFirebaseConfig()) {
|
|
103
|
+
await this._initializeFirebase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Initialize Sentry if enabled
|
|
107
|
+
if (this.config.sentry?.enabled) {
|
|
108
|
+
await this._sentry.init(this.config.sentry.config);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Initialize Analytics if ID and secret are provided
|
|
112
|
+
if (this.config.analytics?.google && this.config.analytics?.googleSecret) {
|
|
113
|
+
this._analytics.init({
|
|
114
|
+
id: this.config.analytics.google,
|
|
115
|
+
secret: this.config.analytics.googleSecret,
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
console.log('[Analytics] Skipped: missing analytics google ID or secret');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Initialize service worker if enabled
|
|
122
|
+
if (this.config.serviceWorker?.enabled) {
|
|
123
|
+
this._serviceWorker.register({
|
|
124
|
+
path: this.config.serviceWorker?.config?.path
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Start version checking if enabled
|
|
129
|
+
if (this.config.refreshNewVersion?.enabled) {
|
|
130
|
+
this._startVersionCheck();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Set up auth event listeners (uses event delegation, no need to wait for DOM)
|
|
134
|
+
this._auth.setupEventListeners();
|
|
135
|
+
|
|
136
|
+
// Set up push notifications
|
|
137
|
+
if (this.config.pushNotifications?.enabled) {
|
|
138
|
+
this._notifications.initialize(this.config.pushNotifications.config);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Initialize Chatsy chat widget if enabled
|
|
142
|
+
if (this.config.chatsy?.enabled && this.config.chatsy?.config?.agentId) {
|
|
143
|
+
this._initializeChatsy();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Old IE force polyfill
|
|
147
|
+
// await this._loadPolyfillsIfNeeded();
|
|
148
|
+
|
|
149
|
+
// Initialize usage tracking
|
|
150
|
+
await this._usage.initialize();
|
|
151
|
+
|
|
152
|
+
// Update bindings with config and usage data
|
|
153
|
+
this.bindings().update({
|
|
154
|
+
config: this.config,
|
|
155
|
+
usage: this._usage.getBindingData(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return this;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Manager initialization error:', error);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_processConfiguration(configuration) {
|
|
166
|
+
// Default configuration structure
|
|
167
|
+
const defaults = {
|
|
168
|
+
runtime: null, // Auto-detect if not provided (web, browser-extension, electron, node)
|
|
169
|
+
environment: 'production',
|
|
170
|
+
buildTime: Date.now(),
|
|
171
|
+
brand: {
|
|
172
|
+
id: 'brand',
|
|
173
|
+
name: 'Brand',
|
|
174
|
+
description: '',
|
|
175
|
+
type: 'Organization',
|
|
176
|
+
images: {
|
|
177
|
+
brandmark: '',
|
|
178
|
+
wordmark: '',
|
|
179
|
+
combomark: ''
|
|
180
|
+
},
|
|
181
|
+
contact: {
|
|
182
|
+
email: '',
|
|
183
|
+
phone: '',
|
|
184
|
+
'slapform-form-id': ''
|
|
185
|
+
},
|
|
186
|
+
address: {}
|
|
187
|
+
},
|
|
188
|
+
auth: {
|
|
189
|
+
enabled: true,
|
|
190
|
+
config: {
|
|
191
|
+
policy: null,
|
|
192
|
+
redirects: {
|
|
193
|
+
authenticated: '/account',
|
|
194
|
+
unauthenticated: '/signup'
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
firebase: {
|
|
199
|
+
app: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
config: {}
|
|
202
|
+
},
|
|
203
|
+
appCheck: {
|
|
204
|
+
enabled: false,
|
|
205
|
+
config: {
|
|
206
|
+
siteKey: ''
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
cookieConsent: {
|
|
211
|
+
enabled: true,
|
|
212
|
+
config: {
|
|
213
|
+
palette: {
|
|
214
|
+
popup: {
|
|
215
|
+
background: '#237afc',
|
|
216
|
+
text: '#fff'
|
|
217
|
+
},
|
|
218
|
+
button: {
|
|
219
|
+
background: '#fff',
|
|
220
|
+
text: '#237afc'
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
theme: 'classic',
|
|
224
|
+
position: 'bottom-left',
|
|
225
|
+
// type: '',
|
|
226
|
+
// showLink: false,
|
|
227
|
+
content: {
|
|
228
|
+
message: 'We use cookies to ensure you get the best experience on our website. By continuing to use the site, you agree to our { terms }.',
|
|
229
|
+
dismiss: 'I Understand'
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
chatsy: {
|
|
234
|
+
enabled: false,
|
|
235
|
+
config: {
|
|
236
|
+
agentId: '',
|
|
237
|
+
settings: {
|
|
238
|
+
button: {
|
|
239
|
+
backgroundColor: '#237afc',
|
|
240
|
+
textColor: '#FFFFFF',
|
|
241
|
+
position: 'bottom-right',
|
|
242
|
+
type: 'round',
|
|
243
|
+
icon: 'default',
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
sentry: {
|
|
249
|
+
enabled: true,
|
|
250
|
+
config: {
|
|
251
|
+
dsn: '',
|
|
252
|
+
release: '',
|
|
253
|
+
replaysSessionSampleRate: 0.01,
|
|
254
|
+
replaysOnErrorSampleRate: 0.01
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
exitPopup: {
|
|
258
|
+
enabled: true,
|
|
259
|
+
config: {
|
|
260
|
+
timeout: 1000 * 60 * 60 * 4,
|
|
261
|
+
title: 'Want 15% off?',
|
|
262
|
+
message: 'Get 15% off your purchase of our Premium plans.',
|
|
263
|
+
okButton: {
|
|
264
|
+
text: 'Claim 15% Discount',
|
|
265
|
+
link: '/pricing'
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
lazyLoading: {
|
|
270
|
+
enabled: true,
|
|
271
|
+
config: {
|
|
272
|
+
selector: '[data-lazy]',
|
|
273
|
+
rootMargin: '50px 0px', // Start loading 50px before element comes into view
|
|
274
|
+
threshold: 0.01, // Trigger when 1% of element is visible
|
|
275
|
+
loadedClass: 'lazy-loaded',
|
|
276
|
+
loadingClass: 'lazy-loading',
|
|
277
|
+
errorClass: 'lazy-error'
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
socialSharing: {
|
|
281
|
+
enabled: false,
|
|
282
|
+
config: {
|
|
283
|
+
selector: '[data-social-share]',
|
|
284
|
+
defaultPlatforms: ['facebook', 'twitter', 'linkedin', 'pinterest', 'reddit', 'email', 'copy'],
|
|
285
|
+
buttonClass: '',
|
|
286
|
+
showLabels: false,
|
|
287
|
+
openInNewWindow: true,
|
|
288
|
+
windowWidth: 600,
|
|
289
|
+
windowHeight: 400,
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
pushNotifications: {
|
|
293
|
+
enabled: true,
|
|
294
|
+
config: {
|
|
295
|
+
autoRequest: 1000 * 60
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
env: {
|
|
299
|
+
FIREBASE_EMULATOR_CONNECT: false,
|
|
300
|
+
},
|
|
301
|
+
validRedirectHosts: [],
|
|
302
|
+
payment: {
|
|
303
|
+
processors: {},
|
|
304
|
+
products: [],
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// Non-configurable defaults
|
|
308
|
+
refreshNewVersion: {
|
|
309
|
+
enabled: true,
|
|
310
|
+
config: {
|
|
311
|
+
interval: 1000 * 60 * 60, // Check every hour
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
serviceWorker: {
|
|
315
|
+
enabled: true,
|
|
316
|
+
config: {
|
|
317
|
+
path: '/service-worker.js'
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
analytics: {
|
|
321
|
+
google: '',
|
|
322
|
+
googleSecret: '',
|
|
323
|
+
meta: '',
|
|
324
|
+
tiktok: '',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Deep merge configuration with defaults
|
|
329
|
+
const merged = this._deepMerge(defaults, configuration);
|
|
330
|
+
|
|
331
|
+
// Evaluate string expressions for timeout values
|
|
332
|
+
if (merged.exitPopup?.config?.timeout) {
|
|
333
|
+
merged.exitPopup.config.timeout = safeEvaluate(merged.exitPopup.config.timeout);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (merged.pushNotifications?.config?.autoRequest) {
|
|
337
|
+
merged.pushNotifications.config.autoRequest = safeEvaluate(merged.pushNotifications.config.autoRequest);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (merged.refreshNewVersion?.config?.interval) {
|
|
341
|
+
merged.refreshNewVersion.config.interval = safeEvaluate(merged.refreshNewVersion.config.interval);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Calculate buildTimeISO from buildTime
|
|
345
|
+
if (merged.buildTime) {
|
|
346
|
+
merged.buildTimeISO = new Date(merged.buildTime).toISOString();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Return merged configuration
|
|
350
|
+
return merged;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_deepMerge(target, source) {
|
|
354
|
+
const output = Object.assign({}, target);
|
|
355
|
+
if (isObject(target) && isObject(source)) {
|
|
356
|
+
Object.keys(source).forEach(key => {
|
|
357
|
+
if (isObject(source[key])) {
|
|
358
|
+
if (!(key in target))
|
|
359
|
+
Object.assign(output, { [key]: source[key] });
|
|
360
|
+
else
|
|
361
|
+
output[key] = this._deepMerge(target[key], source[key]);
|
|
362
|
+
} else {
|
|
363
|
+
Object.assign(output, { [key]: source[key] });
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return output;
|
|
368
|
+
|
|
369
|
+
function isObject(item) {
|
|
370
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_setHtmlDataAttributes() {
|
|
375
|
+
// Skip if not in browser environment
|
|
376
|
+
if (typeof document === 'undefined') {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const $html = document.documentElement;
|
|
381
|
+
|
|
382
|
+
// Set platform (OS) - windows, mac, linux, ios, android, chromeos, unknown
|
|
383
|
+
$html.dataset.platform = this._utilities.getPlatform();
|
|
384
|
+
|
|
385
|
+
// Set browser - chrome, firefox, safari, edge, opera, brave
|
|
386
|
+
$html.dataset.browser = this._utilities.getBrowser();
|
|
387
|
+
|
|
388
|
+
// Set runtime - web, browser-extension, electron, node
|
|
389
|
+
$html.dataset.runtime = this._utilities.getRuntime();
|
|
390
|
+
|
|
391
|
+
// Set device - mobile, tablet, desktop
|
|
392
|
+
$html.dataset.device = this._utilities.getDevice();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Resolve the Firebase web SDK config blob. Flat `firebaseConfig` first (canonical
|
|
396
|
+
// shape — BEM/BXM/EM), then nested `firebase.app.config` (UJM legacy yaml shape).
|
|
397
|
+
// Returns the blob when it has at least one own key, otherwise null.
|
|
398
|
+
_resolveFirebaseConfig() {
|
|
399
|
+
const flat = this.config.firebaseConfig;
|
|
400
|
+
if (flat && typeof flat === 'object' && Object.keys(flat).length > 0) {
|
|
401
|
+
return flat;
|
|
402
|
+
}
|
|
403
|
+
const nested = this.config.firebase?.app?.config;
|
|
404
|
+
if (nested && typeof nested === 'object' && Object.keys(nested).length > 0) {
|
|
405
|
+
return nested;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async _initializeFirebase() {
|
|
411
|
+
const firebaseConfig = this._resolveFirebaseConfig();
|
|
412
|
+
|
|
413
|
+
// Dynamically import Firebase v12
|
|
414
|
+
const { initializeApp } = await import('firebase/app');
|
|
415
|
+
const { getAuth, onAuthStateChanged } = await import('firebase/auth');
|
|
416
|
+
const { initializeFirestore } = await import('firebase/firestore');
|
|
417
|
+
const { getMessaging } = await import('firebase/messaging');
|
|
418
|
+
|
|
419
|
+
// If we're in devmode, set the firebase config authDomain to the current host
|
|
420
|
+
// if (this.isDevelopment() && firebaseConfig) {
|
|
421
|
+
// firebaseConfig.authDomain = window.location.hostname;
|
|
422
|
+
// }
|
|
423
|
+
|
|
424
|
+
// Initialize Firebase. Re-init guards: if there's already a [DEFAULT] app
|
|
425
|
+
// (live reload, re-init in tests), get the existing one rather than throwing
|
|
426
|
+
// `app/duplicate-app`.
|
|
427
|
+
const { getApp, getApps } = await import('firebase/app');
|
|
428
|
+
const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
|
|
429
|
+
|
|
430
|
+
// Store Firebase references
|
|
431
|
+
this._firebaseApp = app;
|
|
432
|
+
this._firebaseAuth = getAuth(app);
|
|
433
|
+
this._firebaseFirestore = initializeFirestore(app, {});
|
|
434
|
+
|
|
435
|
+
// Only initialize messaging if service workers are supported
|
|
436
|
+
if ('serviceWorker' in navigator) {
|
|
437
|
+
this._firebaseMessaging = getMessaging(app);
|
|
438
|
+
} else {
|
|
439
|
+
console.warn('Service workers not available - Firebase Messaging disabled');
|
|
440
|
+
this._firebaseMessaging = null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Initialize Firebase App Check if enabled
|
|
444
|
+
if (this.config.firebase.appCheck?.enabled) {
|
|
445
|
+
const { initializeAppCheck, ReCaptchaEnterpriseProvider } = await import('firebase/app-check');
|
|
446
|
+
const siteKey = this.config.firebase.appCheck.config.siteKey;
|
|
447
|
+
|
|
448
|
+
if (siteKey) {
|
|
449
|
+
initializeAppCheck(app, {
|
|
450
|
+
provider: new ReCaptchaEnterpriseProvider(siteKey),
|
|
451
|
+
isTokenAutoRefreshEnabled: true
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Setup auth state listener
|
|
457
|
+
onAuthStateChanged(this._firebaseAuth, (user) => {
|
|
458
|
+
// Mark auth as initialized and resolve the settler promise on first callback
|
|
459
|
+
if (!this._firebaseAuthInitialized) {
|
|
460
|
+
this._firebaseAuthInitialized = true;
|
|
461
|
+
this._authReadyResolve();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Let auth module handle everything including DOM updates
|
|
465
|
+
this._auth._handleAuthStateChange(user);
|
|
466
|
+
|
|
467
|
+
// Update Chatsy with current user
|
|
468
|
+
if (this._chatsy) {
|
|
469
|
+
const resolved = this._auth.getUser();
|
|
470
|
+
this._chatsy.setUser(resolved ? { id: resolved.uid, email: resolved.email, firstName: resolved.displayName, photoURL: resolved.photoURL } : null);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Getters for Firebase services
|
|
476
|
+
get firebaseApp() { return this._firebaseApp; }
|
|
477
|
+
get firebaseAuth() { return this._firebaseAuth; }
|
|
478
|
+
get firebaseFirestore() { return this._firebaseFirestore; }
|
|
479
|
+
get firebaseMessaging() { return this._firebaseMessaging; }
|
|
480
|
+
|
|
481
|
+
isDevelopment() {
|
|
482
|
+
return this.config.environment === 'development';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
getFunctionsUrl(environment) {
|
|
486
|
+
const env = environment || this.config.environment;
|
|
487
|
+
const projectId = this._resolveFirebaseConfig()?.projectId;
|
|
488
|
+
|
|
489
|
+
if (!projectId) {
|
|
490
|
+
throw new Error('Firebase project ID not configured');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (env === 'development') {
|
|
494
|
+
return 'http://localhost:5001/' + projectId + '/us-central1';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return 'https://us-central1-' + projectId + '.cloudfunctions.net';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
getApiUrl(environment, url) {
|
|
501
|
+
// Precedence: passed environment > query string > config.environment
|
|
502
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
503
|
+
const queryEnv = searchParams.get('_dev_apiEnvironment');
|
|
504
|
+
const env = environment
|
|
505
|
+
|| queryEnv
|
|
506
|
+
|| this.config.environment;
|
|
507
|
+
|
|
508
|
+
if (env === 'development') {
|
|
509
|
+
// BEM's `mgr serve` exposes the local API over HTTPS (mkcert proxy on 5002,
|
|
510
|
+
// since backend-manager 5.7.0) — plain http:// cannot connect to it.
|
|
511
|
+
return 'https://localhost:5002';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const apiDomain = this._resolveFirebaseConfig()?.authDomain; // Has to be this since some projects like Clockii use ITW Universal Auth
|
|
515
|
+
// const apiDomain = this.config.brand.url;
|
|
516
|
+
const baseUrl = url || (apiDomain ? `https://${apiDomain}` : window.location.origin);
|
|
517
|
+
const urlObj = new URL(baseUrl);
|
|
518
|
+
|
|
519
|
+
// Prepend 'api.' subdomain (universal-auth.itwcreativeworks.com -> api.universal-auth.itwcreativeworks.com)
|
|
520
|
+
urlObj.hostname = `api.${urlObj.hostname}`;
|
|
521
|
+
|
|
522
|
+
// Strip trailing slash
|
|
523
|
+
return urlObj.toString().replace(/\/$/, '');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
isValidRedirectUrl(url) {
|
|
527
|
+
try {
|
|
528
|
+
const returnUrlObject = new URL(decodeURIComponent(url));
|
|
529
|
+
const currentUrlObject = new URL(window.location.href);
|
|
530
|
+
|
|
531
|
+
// Loopback returns (RFC 8252 §7.3) are valid while the SITE runs in development:
|
|
532
|
+
// native apps (Electron Manager) can't OS-register their custom scheme in dev, so
|
|
533
|
+
// their sign-in flow returns to an ephemeral 127.0.0.1 listener instead. Any port —
|
|
534
|
+
// the app binds it at flow start. Production sites never match this branch.
|
|
535
|
+
if (this.isDevelopment() && ['127.0.0.1', '[::1]', 'localhost'].includes(returnUrlObject.hostname)) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return returnUrlObject.host === currentUrlObject.host
|
|
540
|
+
|| returnUrlObject.protocol === this.config.brand?.id + ':'
|
|
541
|
+
|| (this.config.validRedirectHosts || []).includes(returnUrlObject.host);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async _initializeChatsy() {
|
|
548
|
+
try {
|
|
549
|
+
const { default: Chatsy } = await import('chatsy');
|
|
550
|
+
const config = this.config.chatsy.config;
|
|
551
|
+
|
|
552
|
+
this._chatsy = new Chatsy(config.agentId, {
|
|
553
|
+
settings: config.settings,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
console.log('[Chatsy] Initialized');
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error('[Chatsy] Failed to initialize:', error);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
_startVersionCheck() {
|
|
563
|
+
// Quit if window is not available
|
|
564
|
+
if (typeof window !== 'undefined') {
|
|
565
|
+
// Re-focus events
|
|
566
|
+
window.addEventListener('focus', () => {
|
|
567
|
+
this._checkVersion();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
window.addEventListener('online', () => {
|
|
571
|
+
this._checkVersion();
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Set up interval
|
|
576
|
+
this._versionCheckInterval = setInterval(() => {
|
|
577
|
+
this._checkVersion();
|
|
578
|
+
}, this.config.refreshNewVersion.config.interval);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
// async _loadPolyfillsIfNeeded() {
|
|
583
|
+
// // Check if polyfills are needed by testing for ES6 features
|
|
584
|
+
// const featuresPass = (
|
|
585
|
+
// typeof Symbol !== 'undefined'
|
|
586
|
+
// );
|
|
587
|
+
|
|
588
|
+
// // If all features are supported, no polyfills needed
|
|
589
|
+
// if (featuresPass) {
|
|
590
|
+
// return;
|
|
591
|
+
// }
|
|
592
|
+
|
|
593
|
+
// // Load polyfills for older browsers (especially IE)
|
|
594
|
+
// try {
|
|
595
|
+
// await domUtils.loadScript({
|
|
596
|
+
// src: 'https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?flags=always%2Cgated&features=default%2Ces5%2Ces6%2Ces7%2CPromise.prototype.finally%2C%7Ehtml5-elements%2ClocalStorage%2Cfetch%2CURLSearchParams',
|
|
597
|
+
// crossorigin: 'anonymous'
|
|
598
|
+
// });
|
|
599
|
+
// console.log('Polyfills loaded for older browser');
|
|
600
|
+
// } catch (error) {
|
|
601
|
+
// console.error('Failed to load polyfills:', error);
|
|
602
|
+
// // Continue initialization even if polyfills fail to load
|
|
603
|
+
// }
|
|
604
|
+
// }
|
|
605
|
+
|
|
606
|
+
async _checkVersion() {
|
|
607
|
+
if (this.isDevelopment()) {
|
|
608
|
+
/* @dev-only:start */
|
|
609
|
+
{
|
|
610
|
+
console.log('[Version] Skipping version check in development mode');
|
|
611
|
+
}
|
|
612
|
+
/* @dev-only:end */
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
// Try new path first, then fallback to legacy path
|
|
618
|
+
const paths = ['/build.json', '/@output/build/build.json'];
|
|
619
|
+
let data = null;
|
|
620
|
+
let response = null;
|
|
621
|
+
|
|
622
|
+
for (const path of paths) {
|
|
623
|
+
try {
|
|
624
|
+
response = await fetch(`${path}?cb=${Date.now()}`);
|
|
625
|
+
if (response.ok) {
|
|
626
|
+
data = await response.json();
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
} catch (e) {
|
|
630
|
+
// Continue to next path
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!data) {
|
|
635
|
+
throw new Error('Failed to fetch build.json from any path');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Extract timestamp - support both new format (data.timestamp) and legacy format (data['npm-build'].timestamp)
|
|
639
|
+
let buildTimeLive;
|
|
640
|
+
if (data.timestamp) {
|
|
641
|
+
buildTimeLive = new Date(data.timestamp);
|
|
642
|
+
} else if (data?.['npm-build']?.timestamp) {
|
|
643
|
+
buildTimeLive = new Date(data['npm-build'].timestamp);
|
|
644
|
+
} else {
|
|
645
|
+
throw new Error('No timestamp found in build.json');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const buildTimeCurrent = new Date(this.config.buildTime);
|
|
649
|
+
|
|
650
|
+
// Add 1 hour to current build time to account for npm build process
|
|
651
|
+
buildTimeCurrent.setHours(buildTimeCurrent.getHours() + 1);
|
|
652
|
+
|
|
653
|
+
// Log version info
|
|
654
|
+
console.log(`[Version] Current build time: ${buildTimeCurrent.toISOString()}, Live build time: ${buildTimeLive.toISOString()}`);
|
|
655
|
+
|
|
656
|
+
// If live version is newer, reload the page
|
|
657
|
+
if (buildTimeCurrent >= buildTimeLive) {
|
|
658
|
+
return; // No update needed
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// New version detected
|
|
662
|
+
console.log('[Version] New version detected, reloading page...');
|
|
663
|
+
|
|
664
|
+
// If running in a non-browser environment, warn and return
|
|
665
|
+
if (typeof window === 'undefined') {
|
|
666
|
+
console.warn('[Version] Cannot reload in non-browser environment');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Force page reload
|
|
671
|
+
window.onbeforeunload = function () {
|
|
672
|
+
return undefined;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
window.location.reload(true);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.warn('[Version] Failed version check:', error);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Safely evaluate timeout string expressions
|
|
683
|
+
const safeEvaluate = (str) => {
|
|
684
|
+
if (typeof str !== 'string') return str;
|
|
685
|
+
|
|
686
|
+
// Only allow numbers, *, +, -, /, parentheses, and whitespace
|
|
687
|
+
if (!/^[\d\s\*\+\-\/\(\)]+$/.test(str)) {
|
|
688
|
+
console.warn('Invalid expression format:', str);
|
|
689
|
+
return str;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
// Use Function constructor instead of eval for safer evaluation
|
|
694
|
+
return new Function('return ' + str)();
|
|
695
|
+
} catch (e) {
|
|
696
|
+
console.warn('Failed to evaluate expression:', str, e);
|
|
697
|
+
return str;
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Create singleton instance
|
|
702
|
+
const manager = new Manager();
|
|
703
|
+
|
|
704
|
+
// Export for different environments
|
|
705
|
+
export default manager;
|
|
706
|
+
export { Manager };
|
|
707
|
+
|
|
708
|
+
// For non-ES6 environments
|
|
709
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
710
|
+
module.exports = manager;
|
|
711
|
+
}
|
|
712
|
+
|