web-manager 4.0.0 → 4.0.1

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/CHANGELOG.md CHANGED
@@ -15,6 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
17
  ---
18
+ ## [4.0.0] - 2025-09-11
19
+ ### BREAKING
20
+ - Updated to ITW 3.0 standard.
21
+
18
22
  ## [3.2.74] - 2025-07-17
19
23
  ### Added
20
24
  - Now looks for `build.json` in the `/@output/build/` directory to ensure it works with Vite's output structure.
@@ -0,0 +1,347 @@
1
+ class ServiceWorker {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this._registration = null;
5
+ this._updateCallbacks = [];
6
+ this._messageHandlers = new Map();
7
+ }
8
+
9
+ // Check if service workers are supported
10
+ isSupported() {
11
+ return 'serviceWorker' in navigator;
12
+ }
13
+
14
+ // Register service worker
15
+ async register(options = {}) {
16
+ try {
17
+ if (!this.isSupported()) {
18
+ console.warn('Service Workers are not supported');
19
+ return null;
20
+ }
21
+
22
+ const swPath = options.path || this.manager.config.serviceWorker?.config?.path || '/service-worker.js';
23
+ const scope = options.scope || '/';
24
+
25
+ // Build config object to pass to service worker
26
+ const config = {
27
+ app: this.manager.config.brand?.id,
28
+ environment: this.manager.config.environment,
29
+ buildTime: this.manager.config.buildTime,
30
+ firebase: this.manager.config.firebase?.app?.config || null
31
+ };
32
+
33
+ // Get service worker URL with just cache breaker
34
+ const cacheBreaker = config.buildTime || Date.now();
35
+ const swUrl = `${swPath}?cb=${cacheBreaker}`;
36
+
37
+ // Get existing registrations
38
+ const registrations = await navigator.serviceWorker.getRegistrations();
39
+
40
+ // Check if service worker is already registered for this scope
41
+ let registration = registrations.find(reg =>
42
+ reg.scope === new URL(scope, window.location.href).href
43
+ );
44
+
45
+ // Always register/re-register to ensure we have the correct URL with config
46
+ console.log('----WM SW 1');
47
+
48
+ if (registration) {
49
+ console.log('----WM SW 2');
50
+ console.log('Unregistering existing service worker to update config');
51
+ await registration.unregister();
52
+ // Wait a bit for unregistration to complete
53
+ await new Promise(resolve => setTimeout(resolve, 100));
54
+ }
55
+ console.log('----WM SW 3');
56
+
57
+ console.log('Registering service worker with cache breaker');
58
+ registration = await navigator.serviceWorker.register(swUrl, {
59
+ scope,
60
+ updateViaCache: options.updateViaCache || 'imports'
61
+ });
62
+
63
+ // Wait for the service worker to be ready
64
+ await navigator.serviceWorker.ready;
65
+
66
+ // Send the full config via postMessage after registration
67
+ console.log('----WM SW 4');
68
+ if (registration.active) {
69
+ console.log('----WM SW 5');
70
+ try {
71
+ this.postMessage({
72
+ command: 'update-config',
73
+ payload: config
74
+ });
75
+ } catch (error) {
76
+ console.warn('Could not send config to service worker:', error);
77
+ }
78
+ }
79
+
80
+ this._registration = registration;
81
+ this.manager.state.serviceWorker = registration;
82
+ console.log('----WM SW 6');
83
+
84
+ // Set up update handlers
85
+ this._setupUpdateHandlers(registration);
86
+ console.log('----WM SW 7');
87
+
88
+ // Check for updates
89
+ if (options.checkForUpdate !== false) {
90
+ registration.update();
91
+ }
92
+ console.log('----WM SW 8');
93
+
94
+ // Set up message channel
95
+ if (registration.active) {
96
+ this._setupMessageChannel();
97
+ }
98
+ console.log('----WM SW 9');
99
+
100
+ return registration;
101
+ } catch (error) {
102
+ console.error('Service Worker registration failed:', error);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ // Unregister service worker
108
+ async unregister() {
109
+ try {
110
+ if (!this._registration) {
111
+ const registrations = await navigator.serviceWorker.getRegistrations();
112
+ for (const registration of registrations) {
113
+ await registration.unregister();
114
+ }
115
+ } else {
116
+ await this._registration.unregister();
117
+ }
118
+
119
+ this._registration = null;
120
+ this.manager.state.serviceWorker = null;
121
+
122
+ return true;
123
+ } catch (error) {
124
+ console.error('Service Worker unregistration failed:', error);
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // Get current registration
130
+ getRegistration() {
131
+ return this._registration;
132
+ }
133
+
134
+ // Check for updates
135
+ async update() {
136
+ try {
137
+ if (!this._registration) {
138
+ throw new Error('No service worker registered');
139
+ }
140
+
141
+ await this._registration.update();
142
+ return true;
143
+ } catch (error) {
144
+ console.error('Service Worker update failed:', error);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ // Post message to service worker
150
+ postMessage(message, options = {}) {
151
+ return new Promise((resolve, reject) => {
152
+ if (!this.isSupported()) {
153
+ return reject(new Error('Service Workers not supported'));
154
+ }
155
+
156
+ const controller = this._registration?.active || navigator.serviceWorker.controller;
157
+
158
+ if (!controller) {
159
+ return reject(new Error('No active service worker'));
160
+ }
161
+
162
+ const messageChannel = new MessageChannel();
163
+ const timeout = options.timeout || 5000;
164
+ let timeoutId;
165
+
166
+ // Set up timeout
167
+ if (timeout > 0) {
168
+ timeoutId = setTimeout(() => {
169
+ messageChannel.port1.close();
170
+ reject(new Error('Service worker message timeout'));
171
+ }, timeout);
172
+ }
173
+
174
+ // Listen for response
175
+ messageChannel.port1.onmessage = (event) => {
176
+ clearTimeout(timeoutId);
177
+
178
+ if (event.data.error) {
179
+ reject(new Error(event.data.error));
180
+ } else {
181
+ resolve(event.data);
182
+ }
183
+ };
184
+
185
+ // Send message
186
+ controller.postMessage(message, [messageChannel.port2]);
187
+ });
188
+ }
189
+
190
+ // Listen for messages from service worker
191
+ onMessage(type, handler) {
192
+ if (!this.isSupported()) {
193
+ return () => {};
194
+ }
195
+
196
+ // Store handler
197
+ if (!this._messageHandlers.has(type)) {
198
+ this._messageHandlers.set(type, new Set());
199
+ }
200
+ this._messageHandlers.get(type).add(handler);
201
+
202
+ // Set up global message listener if not already done
203
+ if (this._messageHandlers.size === 1) {
204
+ navigator.serviceWorker.addEventListener('message', this._handleMessage.bind(this));
205
+ }
206
+
207
+ // Return unsubscribe function
208
+ return () => {
209
+ const handlers = this._messageHandlers.get(type);
210
+ if (handlers) {
211
+ handlers.delete(handler);
212
+ if (handlers.size === 0) {
213
+ this._messageHandlers.delete(type);
214
+ }
215
+ }
216
+ };
217
+ }
218
+
219
+ // Skip waiting and activate new service worker
220
+ async skipWaiting() {
221
+ try {
222
+ if (!this._registration?.waiting) {
223
+ throw new Error('No service worker waiting');
224
+ }
225
+
226
+ // Post message to skip waiting
227
+ await this.postMessage({ action: 'skipWaiting' });
228
+
229
+ // Reload page after activation
230
+ let refreshing = false;
231
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
232
+ if (!refreshing) {
233
+ refreshing = true;
234
+ window.location.reload();
235
+ }
236
+ });
237
+
238
+ return true;
239
+ } catch (error) {
240
+ console.error('Skip waiting failed:', error);
241
+ return false;
242
+ }
243
+ }
244
+
245
+ // Listen for update events
246
+ onUpdateFound(callback) {
247
+ this._updateCallbacks.push(callback);
248
+
249
+ return () => {
250
+ const index = this._updateCallbacks.indexOf(callback);
251
+ if (index > -1) {
252
+ this._updateCallbacks.splice(index, 1);
253
+ }
254
+ };
255
+ }
256
+
257
+ // Get service worker state
258
+ getState() {
259
+ if (!this._registration) {
260
+ return 'none';
261
+ }
262
+
263
+ if (this._registration.installing) {
264
+ return 'installing';
265
+ } else if (this._registration.waiting) {
266
+ return 'waiting';
267
+ } else if (this._registration.active) {
268
+ return 'active';
269
+ }
270
+
271
+ return 'unknown';
272
+ }
273
+
274
+ // Private: Set up update handlers
275
+ _setupUpdateHandlers(registration) {
276
+ // Listen for updates
277
+ registration.addEventListener('updatefound', () => {
278
+ const newWorker = registration.installing;
279
+
280
+ if (!newWorker) return;
281
+
282
+ newWorker.addEventListener('statechange', () => {
283
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
284
+ // New service worker available
285
+ this._notifyUpdateCallbacks({
286
+ type: 'update-available',
287
+ worker: newWorker
288
+ });
289
+
290
+ // Automatically skip waiting and activate new worker
291
+ if (this.manager.config.serviceWorker?.autoUpdate !== false) {
292
+ this.skipWaiting();
293
+ }
294
+ }
295
+ });
296
+ });
297
+
298
+ // Listen for controller changes
299
+ let refreshing = false;
300
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
301
+ if (!refreshing) {
302
+ this._notifyUpdateCallbacks({
303
+ type: 'controller-change'
304
+ });
305
+ }
306
+ });
307
+ }
308
+
309
+ // Private: Notify update callbacks
310
+ _notifyUpdateCallbacks(event) {
311
+ this._updateCallbacks.forEach(callback => {
312
+ try {
313
+ callback(event);
314
+ } catch (error) {
315
+ console.error('Update callback error:', error);
316
+ }
317
+ });
318
+ }
319
+
320
+ // Private: Handle incoming messages
321
+ _handleMessage(event) {
322
+ const { type, ...data } = event.data || {};
323
+
324
+ if (!type) return;
325
+
326
+ const handlers = this._messageHandlers.get(type);
327
+ if (handlers) {
328
+ handlers.forEach(handler => {
329
+ try {
330
+ handler(data, event);
331
+ } catch (error) {
332
+ console.error('Message handler error:', error);
333
+ }
334
+ });
335
+ }
336
+ }
337
+
338
+ // Private: Set up message channel
339
+ _setupMessageChannel() {
340
+ // This ensures we can communicate with the service worker
341
+ navigator.serviceWorker.ready.then(() => {
342
+ console.log('Service Worker ready for messaging');
343
+ });
344
+ }
345
+ }
346
+
347
+ export default ServiceWorker;
package/dist/index.js CHANGED
@@ -86,7 +86,7 @@ class Manager {
86
86
 
87
87
  // Initialize service worker if enabled
88
88
  if (this.config.serviceWorker?.enabled) {
89
- await this._serviceWorker.register({
89
+ this._serviceWorker.register({
90
90
  path: this.config.serviceWorker?.config?.path
91
91
  });
92
92
  }
@@ -1,8 +1,11 @@
1
+ import resolveAccount from 'resolve-account';
2
+
1
3
  class Auth {
2
4
  constructor(manager) {
3
5
  this.manager = manager;
4
6
  this._authStateCallbacks = [];
5
7
  this._readyCallbacks = [];
8
+ this._hasUpdatedBindings = false;
6
9
  }
7
10
 
8
11
  // Check if user is authenticated
@@ -67,8 +70,14 @@ class Auth {
67
70
 
68
71
  // If Firebase is not enabled, call callback immediately with null
69
72
  if (!this.manager.config.firebase?.app?.enabled) {
70
- callback({ user: null, account: null });
71
- return () => {}; // Return empty unsubscribe function
73
+ // Call callback with null user and empty account
74
+ callback({
75
+ user: null,
76
+ account: resolveAccount({}, {})
77
+ });
78
+
79
+ // Return empty unsubscribe function
80
+ return () => {};
72
81
  }
73
82
 
74
83
  // Function to get current state and call callback
@@ -77,18 +86,34 @@ class Auth {
77
86
  const state = { user: this.getUser() };
78
87
 
79
88
  // Then, add account data if requested and user exists
80
- if (options.account && user && this.manager.firebaseFirestore) {
89
+ // if (options.account && user && this.manager.firebaseFirestore) {
90
+ // Fetch account if the user is logged in AND Firestore is available
91
+ if (user && this.manager.firebaseFirestore) {
81
92
  try {
82
93
  state.account = await this._getAccountData(user.uid);
83
94
  } catch (error) {
84
- state.account = null;
95
+ // Pass error to Sentry
96
+ this.manager.sentry().captureException(new Error('Failed to get account data', { cause: error }));
85
97
  }
86
- } else {
87
- state.account = null;
88
98
  }
89
99
 
90
- // Update bindings with auth data
91
- this.manager.bindings().update(state);
100
+ // Always ensure account is at least a default resolved object
101
+ state.account = state.account || resolveAccount({}, { uid: user?.uid });
102
+
103
+ // Update bindings with auth data (only once across all callbacks)
104
+ // Now ONLY the first listener will update bindings until the next auth state change
105
+ if (!this._hasUpdatedBindings) {
106
+ // Run update
107
+ this.manager.bindings().update(state);
108
+
109
+ // Save to storage
110
+ const storage = this.manager.storage();
111
+ storage.set('user.auth', state.user || null);
112
+ storage.set('user.account', state.account || {});
113
+
114
+ // Mark that we've updated bindings
115
+ this._hasUpdatedBindings = true;
116
+ }
92
117
 
93
118
  // Call the provided callback with the state
94
119
  callback(state);
@@ -97,15 +122,27 @@ class Auth {
97
122
  let hasCalledback = false;
98
123
 
99
124
  // Set up listener for auth state changes
100
- return this.onAuthStateChanged((user) => {
125
+ const unsubscribe = this.onAuthStateChanged((user) => {
126
+ // If once option is set, unsubscribe
127
+ // We have to do this here because unsubscribe is only available after this call
128
+ if (options.once && unsubscribe) {
129
+ unsubscribe();
130
+ return;
131
+ }
132
+
101
133
  // Wait for settled state before first callback
102
134
  if (!hasCalledback && !this.manager._firebaseAuthInitialized) {
103
135
  return; // Auth state not yet determined
104
136
  }
105
137
 
138
+ // Mark that we've called back at least once
106
139
  hasCalledback = true;
140
+
141
+ // Get current state and call the callback
107
142
  getStateAndCallback(user);
108
143
  });
144
+
145
+ return unsubscribe;
109
146
  }
110
147
 
111
148
  // Listen for auth state changes
@@ -128,11 +165,8 @@ class Auth {
128
165
 
129
166
  // Internal method to handle auth state changes
130
167
  _handleAuthStateChange(user) {
131
- // Always update bindings when auth state changes (basic update without account)
132
- this.manager.bindings().update({
133
- user: this.getUser(),
134
- account: null
135
- });
168
+ // Reset bindings flag for new auth state
169
+ this._hasUpdatedBindings = false;
136
170
 
137
171
  // Call all registered callbacks
138
172
  this._authStateCallbacks.forEach(callback => {
@@ -173,6 +207,22 @@ class Auth {
173
207
  }
174
208
  }
175
209
 
210
+ // Sign in with email and password
211
+ async signInWithEmailAndPassword(email, password) {
212
+ try {
213
+ if (!this.manager.firebaseAuth) {
214
+ throw new Error('Firebase Auth is not initialized');
215
+ }
216
+
217
+ const { signInWithEmailAndPassword } = await import('firebase/auth');
218
+ const userCredential = await signInWithEmailAndPassword(this.manager.firebaseAuth, email, password);
219
+ return userCredential.user;
220
+ } catch (error) {
221
+ console.error('Sign in with email and password error:', error);
222
+ throw error;
223
+ }
224
+ }
225
+
176
226
  // Sign out the current user
177
227
  async signOut() {
178
228
  try {
@@ -193,7 +243,6 @@ class Auth {
193
243
  }
194
244
 
195
245
  const { doc, getDoc } = await import('firebase/firestore');
196
- const resolveAccount = (await import('resolve-account')).default;
197
246
 
198
247
  const accountDoc = doc(this.manager.firebaseFirestore, 'users', uid);
199
248
  const snapshot = await getDoc(accountDoc);
@@ -81,14 +81,14 @@ class Bindings {
81
81
  const attrName = attrParts[0];
82
82
  const attrExpression = attrParts.slice(1).join(' ');
83
83
  const attrValue = this._resolvePath(context, attrExpression) || '';
84
-
84
+
85
85
  if (attrValue) {
86
86
  element.setAttribute(attrName, attrValue);
87
87
  } else {
88
88
  element.removeAttribute(attrName);
89
89
  }
90
90
  break;
91
-
91
+
92
92
  case '@text':
93
93
  default:
94
94
  // Set text content (default behavior)
@@ -12,21 +12,16 @@ export function loadScript(options) {
12
12
  defer = false,
13
13
  crossorigin = false,
14
14
  integrity = null,
15
- attributes = [],
15
+ attributes = {},
16
16
  timeout = 60000,
17
- retries = 0
17
+ retries = 0,
18
+ parent = null
18
19
  } = options;
19
20
 
20
21
  if (!src) {
21
22
  return reject(new Error('Script source is required'));
22
23
  }
23
24
 
24
- // Check if script already exists
25
- const existingScript = document.querySelector(`script[src="${src}"]`);
26
- if (existingScript) {
27
- return resolve({ script: existingScript, cached: true });
28
- }
29
-
30
25
  let timeoutId;
31
26
  let retryCount = 0;
32
27
 
@@ -45,10 +40,8 @@ export function loadScript(options) {
45
40
  }
46
41
 
47
42
  // Add custom attributes
48
- attributes.forEach(attr => {
49
- if (attr.name && attr.value !== undefined) {
50
- script.setAttribute(attr.name, attr.value);
51
- }
43
+ Object.keys(attributes).forEach(name => {
44
+ script.setAttribute(name, attributes[name]);
52
45
  });
53
46
 
54
47
  // Set up timeout
@@ -68,11 +61,12 @@ export function loadScript(options) {
68
61
  script.onerror = (error) => {
69
62
  clearTimeout(timeoutId);
70
63
  script.remove();
71
- handleError(new Error(`Failed to load script: ${src}`));
64
+ handleError(new Error(`Failed to load script ${src}`, { cause: error }));
72
65
  };
73
66
 
74
67
  // Append to document
75
- (document.head || document.documentElement).appendChild(script);
68
+ const $targetParent = parent || document.head || document.documentElement;
69
+ $targetParent.appendChild(script);
76
70
  }
77
71
 
78
72
  function handleError(error) {
@@ -11,6 +11,22 @@ class ServiceWorker {
11
11
  return 'serviceWorker' in navigator;
12
12
  }
13
13
 
14
+ // Return promise that resolves when service worker is ready
15
+ async ready() {
16
+ if (!this.isSupported()) {
17
+ throw new Error('Service Workers not supported');
18
+ }
19
+
20
+ // If already registered and active
21
+ if (this._registration?.active) {
22
+ return this._registration;
23
+ }
24
+
25
+ // Wait for service worker to be ready
26
+ const registration = await navigator.serviceWorker.ready;
27
+ return registration;
28
+ }
29
+
14
30
  // Register service worker
15
31
  async register(options = {}) {
16
32
  try {
@@ -30,30 +46,11 @@ class ServiceWorker {
30
46
  firebase: this.manager.config.firebase?.app?.config || null
31
47
  };
32
48
 
33
- // Get service worker URL with config
34
- const swUrl = `${swPath}?config=${encodeURIComponent(JSON.stringify(config))}`;
35
-
36
- // Get existing registrations
37
- const registrations = await navigator.serviceWorker.getRegistrations();
38
-
39
- // Check if service worker is already registered for this scope
40
- let registration = registrations.find(reg =>
41
- reg.scope === new URL(scope, window.location.href).href
42
- );
43
-
44
- // This helps the .register() method NOT HANG FOREVER
45
- if (registration) {
46
- console.log('Using existing service worker registration');
47
- // Check for updates on existing registration
48
- registration.update();
49
- } else {
50
- console.log('Registering new service worker');
51
- // Register with config in URL
52
- registration = await navigator.serviceWorker.register(swUrl, {
53
- scope,
54
- updateViaCache: options.updateViaCache || 'imports'
55
- });
56
- }
49
+ // Register and handle everything
50
+ const registration = await navigator.serviceWorker.register(swPath, {
51
+ scope,
52
+ updateViaCache: 'none' // Always check server for updates
53
+ });
57
54
 
58
55
  this._registration = registration;
59
56
  this.manager.state.serviceWorker = registration;
@@ -61,16 +58,27 @@ class ServiceWorker {
61
58
  // Set up update handlers
62
59
  this._setupUpdateHandlers(registration);
63
60
 
64
- // Check for updates
65
- if (options.checkForUpdate !== false) {
66
- registration.update();
67
- }
68
-
69
- // Set up message channel
61
+ // Wait for service worker to be ready and send config
62
+ await navigator.serviceWorker.ready;
63
+
70
64
  if (registration.active) {
65
+ try {
66
+ this.postMessage({
67
+ command: 'update-config',
68
+ payload: config
69
+ });
70
+ } catch (error) {
71
+ console.warn('Could not send config to service worker:', error);
72
+ }
73
+
71
74
  this._setupMessageChannel();
72
75
  }
73
76
 
77
+ // Check for updates (this will detect if service worker file changed)
78
+ if (options.checkForUpdate !== false) {
79
+ registration.update();
80
+ }
81
+
74
82
  return registration;
75
83
  } catch (error) {
76
84
  console.error('Service Worker registration failed:', error);
@@ -260,7 +268,7 @@ class ServiceWorker {
260
268
  type: 'update-available',
261
269
  worker: newWorker
262
270
  });
263
-
271
+
264
272
  // Automatically skip waiting and activate new worker
265
273
  if (this.manager.config.serviceWorker?.autoUpdate !== false) {
266
274
  this.skipWaiting();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-manager",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
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",
@@ -41,14 +41,13 @@
41
41
  "replace": {}
42
42
  },
43
43
  "dependencies": {
44
- "@sentry/browser": "^10.8.0",
45
- "firebase": "^12.2.1",
44
+ "@sentry/browser": "^10.15.0",
45
+ "firebase": "^12.3.0",
46
46
  "itwcw-package-analytics": "^1.0.6",
47
47
  "lodash": "^4.17.21",
48
- "resolve-account": "^2.0.0"
48
+ "resolve-account": "^2.0.1"
49
49
  },
50
50
  "devDependencies": {
51
- "lodash": "^4.17.21",
52
51
  "mocha": "^8.4.0",
53
52
  "prepare-package": "^1.2.2"
54
53
  }