web-manager 4.0.14 → 4.0.16

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.
@@ -6,8 +6,8 @@ class Notifications {
6
6
 
7
7
  // Check if notifications are supported
8
8
  isSupported() {
9
- return 'Notification' in window &&
10
- 'serviceWorker' in navigator &&
9
+ return 'Notification' in window &&
10
+ 'serviceWorker' in navigator &&
11
11
  this.manager.firebaseMessaging !== undefined;
12
12
  }
13
13
 
@@ -79,7 +79,7 @@ class Notifications {
79
79
  storage.set('notifications', {
80
80
  subscribed: true,
81
81
  token: token,
82
- subscribedAt: new Date().toISOString(),
82
+ timestamp: new Date().toISOString(),
83
83
  uid: this.manager.auth().getUser()?.uid || null
84
84
  });
85
85
 
@@ -102,7 +102,7 @@ class Notifications {
102
102
 
103
103
  const { deleteToken } = await import('firebase/messaging');
104
104
  const messaging = this.manager.firebaseMessaging;
105
-
105
+
106
106
  if (messaging) {
107
107
  await deleteToken(messaging);
108
108
  }
@@ -121,7 +121,7 @@ class Notifications {
121
121
  // Request permission (without subscribing)
122
122
  async requestPermission() {
123
123
  try {
124
- if (!('Notification' in window)) {
124
+ if (!this.isSupported()) {
125
125
  throw new Error('Notifications not supported');
126
126
  }
127
127
 
@@ -147,7 +147,7 @@ class Notifications {
147
147
 
148
148
  const { getToken } = await import('firebase/messaging');
149
149
  const swRegistration = this.manager.state.serviceWorker;
150
-
150
+
151
151
  return await getToken(messaging, {
152
152
  serviceWorkerRegistration: swRegistration
153
153
  });
@@ -166,7 +166,7 @@ class Notifications {
166
166
 
167
167
  const { onMessage } = await import('firebase/messaging');
168
168
  const messaging = this.manager.firebaseMessaging;
169
-
169
+
170
170
  if (!messaging) {
171
171
  return () => {};
172
172
  }
@@ -191,38 +191,55 @@ class Notifications {
191
191
 
192
192
  const { onMessage } = await import('firebase/messaging');
193
193
  const messaging = this.manager.firebaseMessaging;
194
-
194
+
195
195
  if (!messaging) {
196
196
  return () => {};
197
197
  }
198
198
 
199
199
  return onMessage(messaging, (payload) => {
200
200
  console.log('Foreground message received:', payload);
201
-
202
- // Show notification if app is in foreground
203
- if (payload.notification) {
204
- const { title, body, icon, badge, image } = payload.notification;
205
- const clickAction = payload.data?.click_action || payload.notification.click_action;
206
-
201
+
202
+ // Extract notification data - handle both payload.notification and payload.data formats
203
+ const notificationData = payload.notification || payload.data || {};
204
+ const { title, body, icon, badge, image, click_action, url, tag } = notificationData;
205
+
206
+ // Determine the click URL (prioritize click_action, then url, then data.url)
207
+ const clickUrl = click_action || url || payload.data?.click_action || payload.data?.url;
208
+
209
+ // Show notification if we have at least a title
210
+ if (title) {
207
211
  const notification = new Notification(title, {
208
- body,
209
- icon,
210
- badge,
211
- image,
212
- data: payload.data,
213
- requireInteraction: false,
212
+ body: body || '',
213
+ icon: icon || '/favicon.ico',
214
+ badge: badge,
215
+ image: image,
216
+ tag: tag || 'default',
217
+ data: { ...payload.data, clickUrl },
218
+ requireInteraction: true,
219
+ renotify: true
214
220
  });
215
221
 
216
222
  notification.onclick = (event) => {
217
223
  event.preventDefault();
218
- if (clickAction) {
219
- window.open(clickAction, '_blank');
224
+
225
+ // Focus or open the target window
226
+ if (clickUrl) {
227
+ // Try to find an existing window/tab with this URL
228
+ window.focus();
229
+ window.open(clickUrl, '_blank');
230
+ } else {
231
+ // Just focus the current window if no URL
232
+ window.focus();
220
233
  }
234
+
221
235
  notification.close();
222
236
  };
223
237
  }
224
238
 
225
- callback(payload);
239
+ // Call the user's callback with the full payload
240
+ if (callback) {
241
+ callback(payload);
242
+ }
226
243
  });
227
244
  } catch (error) {
228
245
  console.error('Message listener error:', error);
@@ -230,51 +247,136 @@ class Notifications {
230
247
  }
231
248
  }
232
249
 
250
+ // Sync subscription when auth state changes
251
+ async syncSubscription() {
252
+ try {
253
+ // Check if we have a stored notification token
254
+ const storage = this.manager.storage();
255
+ const storedNotification = storage.get('notifications');
256
+
257
+ if (!storedNotification?.token) {
258
+ return false;
259
+ }
260
+
261
+ // Update the subscription in Firestore with current auth state
262
+ await this._saveSubscription(storedNotification.token);
263
+
264
+ // Update local storage with current user ID
265
+ const user = this.manager.auth().getUser();
266
+ storage.set('notifications', {
267
+ ...storedNotification,
268
+ uid: user?.uid || null,
269
+ timestamp: new Date().toISOString()
270
+ });
271
+
272
+ return true;
273
+ } catch (error) {
274
+ console.error('Sync subscription error:', error);
275
+ return false;
276
+ }
277
+ }
278
+
233
279
  // Save subscription to Firestore
234
280
  async _saveSubscription(token) {
235
281
  try {
236
282
  const firestore = this.manager.firebaseFirestore;
237
283
  const user = this.manager.auth().getUser();
238
-
284
+
239
285
  if (!firestore || !token) {
240
286
  return;
241
287
  }
242
288
 
243
- const { doc, setDoc, serverTimestamp } = await import('firebase/firestore');
244
-
245
- const subscriptionData = {
246
- token,
247
- platform: 'web',
248
- userAgent: navigator.userAgent,
249
- tags: ['general'],
250
- created: serverTimestamp(),
251
- updated: serverTimestamp(),
252
- };
253
-
254
- if (user) {
255
- subscriptionData.userId = user.uid;
256
- subscriptionData.userEmail = user.email;
257
- }
289
+ const { doc, getDoc, setDoc, updateDoc, deleteDoc } = await import('firebase/firestore');
290
+ const { getContext } = await import('./utilities.js');
291
+
292
+ const now = new Date();
293
+ const timestamp = now.toISOString();
294
+ const timestampUNIX = Math.floor(now.getTime() / 1000);
295
+
296
+ // Get context for client information
297
+ const context = getContext();
298
+ const clientData = context.client;
299
+
300
+ // Reference to the notification document (ID is the token)
301
+ const notificationRef = doc(firestore, 'notifications', token);
302
+
303
+ // Check if document already exists
304
+ const existingDoc = await getDoc(notificationRef);
305
+ const existingData = existingDoc.exists() ? existingDoc.data() : null;
306
+
307
+ // Determine if we need to update
308
+ const currentUid = user?.uid || null;
309
+ const existingUid = existingData?.uid || null;
310
+ const needsUpdate = existingUid !== currentUid;
311
+
312
+ if (!existingData) {
313
+ // New subscription - create the document
314
+ const subscriptionData = {
315
+ token,
316
+ context: {
317
+ client: clientData
318
+ },
319
+ tags: ['general'],
320
+ created: {
321
+ timestamp,
322
+ timestampUNIX
323
+ },
324
+ updated: {
325
+ timestamp,
326
+ timestampUNIX
327
+ },
328
+ uid: currentUid
329
+ };
330
+
331
+ await setDoc(notificationRef, subscriptionData);
332
+
333
+ } else if (needsUpdate) {
334
+ // Existing subscription needs update (userId changed)
335
+ const updateData = {
336
+ context: {
337
+ client: clientData
338
+ },
339
+ updated: {
340
+ timestamp,
341
+ timestampUNIX
342
+ },
343
+ uid: currentUid
344
+ };
258
345
 
259
- // Save to notifications collection
260
- await setDoc(
261
- doc(firestore, 'notifications', token),
262
- subscriptionData,
263
- { merge: true }
264
- );
346
+ await updateDoc(notificationRef, updateData);
347
+ }
348
+ // If no update needed, do nothing
265
349
 
266
- // If user is authenticated, also save reference in user document
267
- if (user) {
350
+ // Update user's notification reference if authenticated
351
+ if (user && (!existingData || needsUpdate)) {
268
352
  await setDoc(
269
353
  doc(firestore, 'users', user.uid, 'notifications', token),
270
354
  {
271
355
  token,
272
- created: serverTimestamp(),
356
+ created: existingData?.created || {
357
+ timestamp,
358
+ timestampUNIX
359
+ },
360
+ updated: {
361
+ timestamp,
362
+ timestampUNIX
363
+ },
273
364
  active: true
274
365
  },
275
366
  { merge: true }
276
367
  );
277
368
  }
369
+
370
+ // Remove old user reference if user changed
371
+ if (existingUid && existingUid !== currentUid && existingUid !== null) {
372
+ try {
373
+ await deleteDoc(doc(firestore, 'users', existingUid, 'notifications', token));
374
+ } catch (err) {
375
+ // Ignore errors when cleaning up old references
376
+ console.log('Could not clean up old user notification reference:', err.message);
377
+ }
378
+ }
379
+
278
380
  } catch (error) {
279
381
  console.error('Save subscription error:', error);
280
382
  // Don't throw - this is not critical for the subscription process
@@ -282,4 +384,4 @@ class Notifications {
282
384
  }
283
385
  }
284
386
 
285
- export default Notifications;
387
+ export default Notifications;
@@ -60,22 +60,22 @@ export function showNotification(message, options = {}) {
60
60
  // Handle different input types
61
61
  let text = message;
62
62
  let type = options.type || 'info';
63
-
63
+
64
64
  // If message is an Error object, extract message and default to danger
65
65
  if (message instanceof Error) {
66
66
  text = message.message;
67
67
  type = options.type || 'danger';
68
68
  }
69
-
69
+
70
70
  // Handle string as second parameter for backwards compatibility
71
71
  if (typeof options === 'string') {
72
72
  options = { type: options };
73
73
  type = options.type;
74
74
  }
75
-
75
+
76
76
  // Extract options
77
77
  const timeout = options.timeout !== undefined ? options.timeout : 5000;
78
-
78
+
79
79
  const $notification = document.createElement('div');
80
80
  $notification.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-5`;
81
81
  $notification.style.zIndex = '9999';
@@ -116,28 +116,50 @@ export function getContext() {
116
116
  }
117
117
  }
118
118
 
119
+ // Get platform
120
+ function getPlatform() {
121
+ const platform = (navigator.userAgentData?.platform || navigator.platform || 'unknown').toLowerCase();
122
+
123
+ if (/iphone|ipad|ipod/.test(platform)) {
124
+ return 'ios';
125
+ } else if (/android/.test(platform)) {
126
+ return 'android';
127
+ } else if (/win/.test(platform)) {
128
+ return 'windows';
129
+ } else if (/mac/.test(platform)) {
130
+ return 'macos';
131
+ } else if (/linux/.test(platform)) {
132
+ return 'linux';
133
+ } else if (/cros/.test(platform)) {
134
+ return 'chromeos';
135
+ } else {
136
+ return 'unknown';
137
+ }
138
+ }
139
+
119
140
  // Return context information
120
141
  return {
121
142
  client: {
143
+ language: navigator.language,
122
144
  mobile: isMobile(),
145
+ platform: getPlatform(),
146
+ userAgent: navigator.userAgent,
147
+ url: window.location.href,
123
148
  },
124
149
  browser: {
125
- userAgent: navigator.userAgent,
126
- language: navigator.language,
127
- platform: navigator.platform,
128
150
  vendor: navigator.vendor,
129
151
  },
130
- screen: {
131
- width: window.screen?.width,
132
- height: window.screen?.height,
133
- availWidth: window.screen?.availWidth,
134
- availHeight: window.screen?.availHeight,
135
- colorDepth: window.screen?.colorDepth,
136
- pixelRatio: window.devicePixelRatio || 1,
137
- },
138
- viewport: {
139
- width: window.innerWidth,
140
- height: window.innerHeight,
141
- }
152
+ // screen: {
153
+ // width: window.screen?.width,
154
+ // height: window.screen?.height,
155
+ // availWidth: window.screen?.availWidth,
156
+ // availHeight: window.screen?.availHeight,
157
+ // colorDepth: window.screen?.colorDepth,
158
+ // pixelRatio: window.devicePixelRatio || 1,
159
+ // },
160
+ // viewport: {
161
+ // width: window.innerWidth,
162
+ // height: window.innerHeight,
163
+ // }
142
164
  };
143
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-manager",
3
- "version": "4.0.14",
3
+ "version": "4.0.16",
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",