web-manager 3.2.74 → 4.0.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.
@@ -0,0 +1,242 @@
1
+ class Firestore {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this._db = null;
5
+ this._initialized = false;
6
+ this._initPromise = null;
7
+ }
8
+
9
+ async _ensureInitialized() {
10
+ if (this._initialized) {
11
+ return this._db;
12
+ }
13
+
14
+ if (!this._initPromise) {
15
+ this._initPromise = this._initializeFirestore();
16
+ }
17
+
18
+ return this._initPromise;
19
+ }
20
+
21
+ async _initializeFirestore() {
22
+ try {
23
+ // Check if Firebase app is initialized
24
+ if (!this.manager._firebaseApp) {
25
+ throw new Error('Firebase app not initialized. Please initialize Firebase first.');
26
+ }
27
+
28
+ // Dynamically import Firestore
29
+ const { getFirestore, doc: firestoreDoc, collection: firestoreCollection, getDoc, setDoc, updateDoc, deleteDoc, getDocs, query, where, orderBy, limit, startAt, endAt } = await import('firebase/firestore');
30
+
31
+ // Store references for later use
32
+ this._firestoreMethods = {
33
+ getFirestore,
34
+ doc: firestoreDoc,
35
+ collection: firestoreCollection,
36
+ getDoc,
37
+ setDoc,
38
+ updateDoc,
39
+ deleteDoc,
40
+ getDocs,
41
+ query,
42
+ where,
43
+ orderBy,
44
+ limit,
45
+ startAt,
46
+ endAt
47
+ };
48
+
49
+ // Initialize Firestore
50
+ this._db = getFirestore(this.manager._firebaseApp);
51
+ this._initialized = true;
52
+
53
+ return this._db;
54
+ } catch (error) {
55
+ console.error('Failed to initialize Firestore:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ // Main doc() method - supports both 'path/to/doc' and ('collection', 'docId')
61
+ doc(...args) {
62
+ const self = this;
63
+ let docPath;
64
+
65
+ // Handle different argument patterns
66
+ if (args.length === 1 && typeof args[0] === 'string') {
67
+ // Single path string: doc('users/userId')
68
+ docPath = args[0];
69
+ } else if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'string') {
70
+ // Collection and doc ID: doc('users', 'userId')
71
+ docPath = `${args[0]}/${args[1]}`;
72
+ } else {
73
+ throw new Error('Invalid arguments for doc(). Use doc("path/to/doc") or doc("collection", "docId")');
74
+ }
75
+
76
+ return {
77
+ async get() {
78
+ await self._ensureInitialized();
79
+ const docRef = self._firestoreMethods.doc(self._db, docPath);
80
+ const docSnap = await self._firestoreMethods.getDoc(docRef);
81
+
82
+ return {
83
+ exists: () => docSnap.exists(),
84
+ data: () => docSnap.data(),
85
+ id: docSnap.id,
86
+ ref: docRef
87
+ };
88
+ },
89
+
90
+ async set(data, options = {}) {
91
+ await self._ensureInitialized();
92
+ const docRef = self._firestoreMethods.doc(self._db, docPath);
93
+ return await self._firestoreMethods.setDoc(docRef, data, options);
94
+ },
95
+
96
+ async update(data) {
97
+ await self._ensureInitialized();
98
+ const docRef = self._firestoreMethods.doc(self._db, docPath);
99
+ return await self._firestoreMethods.updateDoc(docRef, data);
100
+ },
101
+
102
+ async delete() {
103
+ await self._ensureInitialized();
104
+ const docRef = self._firestoreMethods.doc(self._db, docPath);
105
+ return await self._firestoreMethods.deleteDoc(docRef);
106
+ }
107
+ };
108
+ }
109
+
110
+ // Collection method for queries
111
+ collection(collectionPath) {
112
+ const self = this;
113
+
114
+ return {
115
+ async get() {
116
+ await self._ensureInitialized();
117
+ const collRef = self._firestoreMethods.collection(self._db, collectionPath);
118
+ const querySnapshot = await self._firestoreMethods.getDocs(collRef);
119
+
120
+ return {
121
+ docs: querySnapshot.docs.map(doc => ({
122
+ id: doc.id,
123
+ data: () => doc.data(),
124
+ exists: () => doc.exists(),
125
+ ref: doc.ref
126
+ })),
127
+ size: querySnapshot.size,
128
+ empty: querySnapshot.empty,
129
+ forEach: (callback) => querySnapshot.forEach(callback)
130
+ };
131
+ },
132
+
133
+ where(field, operator, value) {
134
+ return self._buildQuery(collectionPath, [{ type: 'where', field, operator, value }]);
135
+ },
136
+
137
+ orderBy(field, direction = 'asc') {
138
+ return self._buildQuery(collectionPath, [{ type: 'orderBy', field, direction }]);
139
+ },
140
+
141
+ limit(count) {
142
+ return self._buildQuery(collectionPath, [{ type: 'limit', count }]);
143
+ },
144
+
145
+ doc(docId) {
146
+ if (docId) {
147
+ return self.doc(`${collectionPath}/${docId}`);
148
+ }
149
+ // Auto-generate ID if not provided
150
+ return self.doc(`${collectionPath}/${self._generateId()}`);
151
+ }
152
+ };
153
+ }
154
+
155
+ // Build chainable query
156
+ _buildQuery(collectionPath, constraints = []) {
157
+ const self = this;
158
+
159
+ const queryBuilder = {
160
+ where(field, operator, value) {
161
+ constraints.push({ type: 'where', field, operator, value });
162
+ return queryBuilder;
163
+ },
164
+
165
+ orderBy(field, direction = 'asc') {
166
+ constraints.push({ type: 'orderBy', field, direction });
167
+ return queryBuilder;
168
+ },
169
+
170
+ limit(count) {
171
+ constraints.push({ type: 'limit', count });
172
+ return queryBuilder;
173
+ },
174
+
175
+ startAt(...values) {
176
+ constraints.push({ type: 'startAt', values });
177
+ return queryBuilder;
178
+ },
179
+
180
+ endAt(...values) {
181
+ constraints.push({ type: 'endAt', values });
182
+ return queryBuilder;
183
+ },
184
+
185
+ async get() {
186
+ await self._ensureInitialized();
187
+ const collRef = self._firestoreMethods.collection(self._db, collectionPath);
188
+
189
+ // Build query constraints
190
+ const queryConstraints = [];
191
+ for (const constraint of constraints) {
192
+ switch (constraint.type) {
193
+ case 'where':
194
+ queryConstraints.push(self._firestoreMethods.where(constraint.field, constraint.operator, constraint.value));
195
+ break;
196
+ case 'orderBy':
197
+ queryConstraints.push(self._firestoreMethods.orderBy(constraint.field, constraint.direction));
198
+ break;
199
+ case 'limit':
200
+ queryConstraints.push(self._firestoreMethods.limit(constraint.count));
201
+ break;
202
+ case 'startAt':
203
+ queryConstraints.push(self._firestoreMethods.startAt(...constraint.values));
204
+ break;
205
+ case 'endAt':
206
+ queryConstraints.push(self._firestoreMethods.endAt(...constraint.values));
207
+ break;
208
+ }
209
+ }
210
+
211
+ const q = self._firestoreMethods.query(collRef, ...queryConstraints);
212
+ const querySnapshot = await self._firestoreMethods.getDocs(q);
213
+
214
+ return {
215
+ docs: querySnapshot.docs.map(doc => ({
216
+ id: doc.id,
217
+ data: () => doc.data(),
218
+ exists: () => doc.exists(),
219
+ ref: doc.ref
220
+ })),
221
+ size: querySnapshot.size,
222
+ empty: querySnapshot.empty,
223
+ forEach: (callback) => querySnapshot.forEach(callback)
224
+ };
225
+ }
226
+ };
227
+
228
+ return queryBuilder;
229
+ }
230
+
231
+ // Helper to generate document IDs (similar to Firebase auto-generated IDs)
232
+ _generateId() {
233
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
234
+ let id = '';
235
+ for (let i = 0; i < 20; i++) {
236
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
237
+ }
238
+ return id;
239
+ }
240
+ }
241
+
242
+ export default Firestore;
@@ -0,0 +1,285 @@
1
+ class Notifications {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this._requestInProgress = false;
5
+ }
6
+
7
+ // Check if notifications are supported
8
+ isSupported() {
9
+ return 'Notification' in window &&
10
+ 'serviceWorker' in navigator &&
11
+ this.manager.firebaseMessaging !== undefined;
12
+ }
13
+
14
+ // Check if user is subscribed to notifications
15
+ async isSubscribed() {
16
+ try {
17
+ if (!this.isSupported()) {
18
+ return false;
19
+ }
20
+
21
+ return Notification.permission === 'granted';
22
+ } catch (error) {
23
+ console.error('Check subscription error:', error);
24
+ return false;
25
+ }
26
+ }
27
+
28
+ // Subscribe to push notifications
29
+ async subscribe(options = {}) {
30
+ try {
31
+ if (!this.isSupported()) {
32
+ throw new Error('Push notifications are not supported');
33
+ }
34
+
35
+ if (this._requestInProgress) {
36
+ throw new Error('Subscription request already in progress');
37
+ }
38
+
39
+ this._requestInProgress = true;
40
+
41
+ // Get Firebase messaging
42
+ const messaging = this.manager.firebaseMessaging;
43
+ if (!messaging) {
44
+ throw new Error('Firebase Messaging not initialized');
45
+ }
46
+
47
+ // Get service worker registration
48
+ const swRegistration = this.manager.state.serviceWorker;
49
+ if (!swRegistration) {
50
+ throw new Error('Service Worker not registered');
51
+ }
52
+
53
+ // Request notification permission if not already granted
54
+ if (Notification.permission === 'default') {
55
+ const permission = await Notification.requestPermission();
56
+ if (permission !== 'granted') {
57
+ throw new Error('Notification permission denied');
58
+ }
59
+ } else if (Notification.permission === 'denied') {
60
+ throw new Error('Notification permission denied');
61
+ }
62
+
63
+ // Get FCM token
64
+ const { getToken } = await import('firebase/messaging');
65
+ const token = await getToken(messaging, {
66
+ serviceWorkerRegistration: swRegistration,
67
+ vapidKey: options.vapidKey
68
+ });
69
+
70
+ if (!token) {
71
+ throw new Error('Failed to get FCM token');
72
+ }
73
+
74
+ // Save subscription info
75
+ await this._saveSubscription(token);
76
+
77
+ // Track in local storage
78
+ const storage = this.manager.storage();
79
+ storage.set('notifications', {
80
+ subscribed: true,
81
+ token: token,
82
+ subscribedAt: new Date().toISOString(),
83
+ uid: this.manager.auth().getUser()?.uid || null
84
+ });
85
+
86
+ this._requestInProgress = false;
87
+ return { subscribed: true, token };
88
+
89
+ } catch (error) {
90
+ this._requestInProgress = false;
91
+ console.error('Subscribe error:', error);
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ // Unsubscribe from push notifications
97
+ async unsubscribe() {
98
+ try {
99
+ if (!this.isSupported()) {
100
+ return false;
101
+ }
102
+
103
+ const { deleteToken } = await import('firebase/messaging');
104
+ const messaging = this.manager.firebaseMessaging;
105
+
106
+ if (messaging) {
107
+ await deleteToken(messaging);
108
+ }
109
+
110
+ // Clear local storage
111
+ const storage = this.manager.storage();
112
+ storage.remove('notifications');
113
+
114
+ return true;
115
+ } catch (error) {
116
+ console.error('Unsubscribe error:', error);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ // Request permission (without subscribing)
122
+ async requestPermission() {
123
+ try {
124
+ if (!('Notification' in window)) {
125
+ throw new Error('Notifications not supported');
126
+ }
127
+
128
+ const permission = await Notification.requestPermission();
129
+ return permission === 'granted';
130
+ } catch (error) {
131
+ console.error('Request permission error:', error);
132
+ return false;
133
+ }
134
+ }
135
+
136
+ // Get current FCM token
137
+ async getToken() {
138
+ try {
139
+ if (!this.isSupported()) {
140
+ return null;
141
+ }
142
+
143
+ const messaging = this.manager.firebaseMessaging;
144
+ if (!messaging) {
145
+ return null;
146
+ }
147
+
148
+ const { getToken } = await import('firebase/messaging');
149
+ const swRegistration = this.manager.state.serviceWorker;
150
+
151
+ return await getToken(messaging, {
152
+ serviceWorkerRegistration: swRegistration
153
+ });
154
+ } catch (error) {
155
+ console.error('Get token error:', error);
156
+ return null;
157
+ }
158
+ }
159
+
160
+ // Listen for token refresh
161
+ async onTokenRefresh(callback) {
162
+ try {
163
+ if (!this.isSupported()) {
164
+ return () => {};
165
+ }
166
+
167
+ const { onMessage } = await import('firebase/messaging');
168
+ const messaging = this.manager.firebaseMessaging;
169
+
170
+ if (!messaging) {
171
+ return () => {};
172
+ }
173
+
174
+ return onMessage(messaging, (payload) => {
175
+ if (payload.data?.refreshToken) {
176
+ callback(payload.data.token);
177
+ }
178
+ });
179
+ } catch (error) {
180
+ console.error('Token refresh listener error:', error);
181
+ return () => {};
182
+ }
183
+ }
184
+
185
+ // Listen for foreground messages
186
+ async onMessage(callback) {
187
+ try {
188
+ if (!this.isSupported()) {
189
+ return () => {};
190
+ }
191
+
192
+ const { onMessage } = await import('firebase/messaging');
193
+ const messaging = this.manager.firebaseMessaging;
194
+
195
+ if (!messaging) {
196
+ return () => {};
197
+ }
198
+
199
+ return onMessage(messaging, (payload) => {
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
+
207
+ const notification = new Notification(title, {
208
+ body,
209
+ icon,
210
+ badge,
211
+ image,
212
+ data: payload.data,
213
+ requireInteraction: false,
214
+ });
215
+
216
+ notification.onclick = (event) => {
217
+ event.preventDefault();
218
+ if (clickAction) {
219
+ window.open(clickAction, '_blank');
220
+ }
221
+ notification.close();
222
+ };
223
+ }
224
+
225
+ callback(payload);
226
+ });
227
+ } catch (error) {
228
+ console.error('Message listener error:', error);
229
+ return () => {};
230
+ }
231
+ }
232
+
233
+ // Save subscription to Firestore
234
+ async _saveSubscription(token) {
235
+ try {
236
+ const firestore = this.manager.firebaseFirestore;
237
+ const user = this.manager.auth().getUser();
238
+
239
+ if (!firestore || !token) {
240
+ return;
241
+ }
242
+
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
+ }
258
+
259
+ // Save to notifications collection
260
+ await setDoc(
261
+ doc(firestore, 'notifications', token),
262
+ subscriptionData,
263
+ { merge: true }
264
+ );
265
+
266
+ // If user is authenticated, also save reference in user document
267
+ if (user) {
268
+ await setDoc(
269
+ doc(firestore, 'users', user.uid, 'notifications', token),
270
+ {
271
+ token,
272
+ created: serverTimestamp(),
273
+ active: true
274
+ },
275
+ { merge: true }
276
+ );
277
+ }
278
+ } catch (error) {
279
+ console.error('Save subscription error:', error);
280
+ // Don't throw - this is not critical for the subscription process
281
+ }
282
+ }
283
+ }
284
+
285
+ export default Notifications;
@@ -0,0 +1,166 @@
1
+ class SentryModule {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this.initialized = false;
5
+ this.Sentry = null;
6
+ this.config = null;
7
+ }
8
+
9
+ /**
10
+ * Initialize Sentry error tracking
11
+ * @param {Object} config - Sentry configuration object
12
+ * @returns {Promise} Resolves when initialization is complete
13
+ */
14
+ init(config = {}) {
15
+ return new Promise((resolve, reject) => {
16
+ // Dynamically import Sentry to reduce initial bundle size
17
+ import('@sentry/browser')
18
+ .then((SentryModule) => {
19
+ // Store reference and expose globally
20
+ this.Sentry = SentryModule;
21
+ window.Sentry = SentryModule;
22
+
23
+ // Build configuration with our defaults
24
+ this.config = this._buildConfig(config);
25
+
26
+ // Initialize Sentry
27
+ this.Sentry.init(this.config);
28
+ this.initialized = true;
29
+
30
+ resolve({ initialized: true });
31
+ })
32
+ .catch((error) => {
33
+ console.error('[Sentry] Failed to initialize:', error);
34
+ reject(error);
35
+ });
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Build Sentry configuration with defaults and integrations
41
+ * @private
42
+ */
43
+ _buildConfig(userConfig) {
44
+ const config = { ...userConfig };
45
+ const manager = this.manager;
46
+
47
+ // Set release version and environment
48
+ config.release = `${manager.config.brand.id}@${manager.config.buildTime}`;
49
+ config.environment = manager.config.environment || 'production';
50
+ config.integrations = config.integrations || [];
51
+
52
+ // Add browser tracing integration if not already present
53
+ if (!config.integrations.some(i => i.name === 'BrowserTracing')) {
54
+ config.integrations.push(this.Sentry.browserTracingIntegration());
55
+ }
56
+
57
+ // Add replay integration if sample rates are configured
58
+ const hasReplays = (config.replaysSessionSampleRate > 0) ||
59
+ (config.replaysOnErrorSampleRate > 0);
60
+
61
+ if (hasReplays && !config.integrations.some(i => i.name === 'Replay')) {
62
+ config.integrations.push(this.Sentry.replayIntegration({
63
+ maskAllText: false,
64
+ blockAllMedia: false,
65
+ }));
66
+ }
67
+
68
+ // Configure beforeSend to enrich events with user data and session info
69
+ config.beforeSend = (event, hint) => {
70
+ const startTime = this.manager.config.page.startTime || Date.now();
71
+ const hoursSinceStart = (Date.now() - startTime) / (1000 * 3600);
72
+ const storage = this.manager.storage();
73
+
74
+ // Add custom tags
75
+ event.tags = {
76
+ ...event.tags,
77
+ 'process.type': 'browser',
78
+ 'usage.session.hours': hoursSinceStart.toFixed(2),
79
+ };
80
+
81
+ // Add user info from storage
82
+ event.user = {
83
+ ...event.user,
84
+ email: storage.get('user.auth.email', ''),
85
+ uid: storage.get('user.auth.uid', ''),
86
+ };
87
+
88
+ // Log error to console for debugging
89
+ console.error('[Sentry] Caught error:', {
90
+ message: event.message || (event.exception?.values?.[0]?.value) || 'Unknown error',
91
+ level: event.level,
92
+ tags: event.tags,
93
+ user: event.user,
94
+ hint
95
+ });
96
+
97
+ // Block sending in development mode
98
+ if (this.manager.isDevelopment()) {
99
+ console.log('[Sentry] Development mode - not sending to Sentry');
100
+ return null;
101
+ }
102
+
103
+ return event;
104
+ };
105
+
106
+ return config;
107
+ }
108
+
109
+ /**
110
+ * Capture an exception and send to Sentry
111
+ * Safe to call even if Sentry is not initialized
112
+ * @param {Error} error - The error to capture
113
+ * @param {Object} captureContext - Additional context for the error
114
+ * @returns {string|null} Event ID if successful, null otherwise
115
+ */
116
+ captureException(error, captureContext) {
117
+ // Log the error
118
+ console.error('[Sentry] Capturing exception:', error);
119
+
120
+ // Safe to call - won't throw if not initialized
121
+ if (!this.initialized) {
122
+ console.log('[Sentry] Not initialized, skipping capture');
123
+ return null;
124
+ }
125
+
126
+ // Call Sentry to capture the exception
127
+ try {
128
+ return this.Sentry.captureException(error, captureContext);
129
+ } catch (captureError) {
130
+ console.error('[Sentry] Failed to capture exception:', captureError);
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // /**
136
+ // * Capture a message and send to Sentry
137
+ // * Safe to call even if Sentry is not initialized
138
+ // * @param {string} message - The message to capture
139
+ // * @param {string} level - Severity level (debug, info, warning, error, fatal)
140
+ // * @param {Object} captureContext - Additional context for the message
141
+ // * @returns {string|null} Event ID if successful, null otherwise
142
+ // */
143
+ // captureMessage(message, level = 'info', captureContext) {
144
+ // if (!message) {
145
+ // console.warn('[Sentry] captureMessage called with no message');
146
+ // return null;
147
+ // }
148
+
149
+ // console.log(`[Sentry] Capturing message (${level}):`, message);
150
+
151
+ // // Safe to call - won't throw if not initialized
152
+ // if (!this.initialized || !this.Sentry) {
153
+ // console.log('[Sentry] Not initialized, skipping capture');
154
+ // return null;
155
+ // }
156
+
157
+ // try {
158
+ // return this.Sentry.captureMessage(message, level, captureContext);
159
+ // } catch (captureError) {
160
+ // console.error('[Sentry] Failed to capture message:', captureError);
161
+ // return null;
162
+ // }
163
+ // }
164
+ }
165
+
166
+ export default SentryModule;