web-manager 3.2.75 → 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.
@@ -0,0 +1,305 @@
1
+ import resolveAccount from 'resolve-account';
2
+
3
+ class Auth {
4
+ constructor(manager) {
5
+ this.manager = manager;
6
+ this._authStateCallbacks = [];
7
+ this._readyCallbacks = [];
8
+ this._hasUpdatedBindings = false;
9
+ }
10
+
11
+ // Check if user is authenticated
12
+ isAuthenticated() {
13
+ return !!this.getUser();
14
+ }
15
+
16
+ // Get current user
17
+ getUser() {
18
+ const user = this.manager.firebaseAuth?.currentUser;
19
+ if (!user) return null;
20
+
21
+ // Get displayName and photoURL from providerData if not set on main user
22
+ let displayName = user.displayName;
23
+ let photoURL = user.photoURL;
24
+
25
+ // If no displayName or photoURL, check providerData
26
+ if ((!displayName || !photoURL) && user.providerData && user.providerData.length > 0) {
27
+ for (const provider of user.providerData) {
28
+ if (!displayName && provider.displayName) {
29
+ displayName = provider.displayName;
30
+ }
31
+ if (!photoURL && provider.photoURL) {
32
+ photoURL = provider.photoURL;
33
+ }
34
+ // Stop if we found both
35
+ if (displayName && photoURL) break;
36
+ }
37
+ }
38
+
39
+ // If still no displayName, use email or fallback
40
+ if (!displayName) {
41
+ displayName = user.email ? user.email.split('@')[0] : 'User';
42
+ }
43
+
44
+ // If still no photoURL, use a default avatar service
45
+ if (!photoURL) {
46
+ // Use ui-avatars.com which generates avatars from initials
47
+ const name = displayName || user.email.split('@')[0] || 'ME';
48
+ const initials = name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
49
+ photoURL = `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=200&background=random&color=000`;
50
+ }
51
+
52
+ return {
53
+ uid: user.uid,
54
+ email: user.email,
55
+ displayName: displayName,
56
+ photoURL: photoURL,
57
+ emailVerified: user.emailVerified,
58
+ metadata: user.metadata,
59
+ providerData: user.providerData,
60
+ };
61
+ }
62
+
63
+ // Listen for auth state changes (waits for settled state before first callback)
64
+ listen(options = {}, callback) {
65
+ // Handle overloaded signatures - if first param is a function, it's the callback
66
+ if (typeof options === 'function') {
67
+ callback = options;
68
+ options = {};
69
+ }
70
+
71
+ // If Firebase is not enabled, call callback immediately with null
72
+ if (!this.manager.config.firebase?.app?.enabled) {
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 () => {};
81
+ }
82
+
83
+ // Function to get current state and call callback
84
+ const getStateAndCallback = async (user) => {
85
+ // Start with the user which will return null if not authenticated
86
+ const state = { user: this.getUser() };
87
+
88
+ // Then, add account data if requested and user exists
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) {
92
+ try {
93
+ state.account = await this._getAccountData(user.uid);
94
+ } catch (error) {
95
+ // Pass error to Sentry
96
+ this.manager.sentry().captureException(new Error('Failed to get account data', { cause: error }));
97
+ }
98
+ }
99
+
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
+ }
117
+
118
+ // Call the provided callback with the state
119
+ callback(state);
120
+ };
121
+
122
+ let hasCalledback = false;
123
+
124
+ // Set up listener for auth state changes
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
+
133
+ // Wait for settled state before first callback
134
+ if (!hasCalledback && !this.manager._firebaseAuthInitialized) {
135
+ return; // Auth state not yet determined
136
+ }
137
+
138
+ // Mark that we've called back at least once
139
+ hasCalledback = true;
140
+
141
+ // Get current state and call the callback
142
+ getStateAndCallback(user);
143
+ });
144
+
145
+ return unsubscribe;
146
+ }
147
+
148
+ // Listen for auth state changes
149
+ onAuthStateChanged(callback) {
150
+ this._authStateCallbacks.push(callback);
151
+
152
+ // If auth is already initialized, call the callback immediately
153
+ if (this.manager._firebaseAuthInitialized) {
154
+ callback(this.manager.firebaseAuth?.currentUser || null);
155
+ }
156
+
157
+ // Return unsubscribe function
158
+ return () => {
159
+ const index = this._authStateCallbacks.indexOf(callback);
160
+ if (index > -1) {
161
+ this._authStateCallbacks.splice(index, 1);
162
+ }
163
+ };
164
+ }
165
+
166
+ // Internal method to handle auth state changes
167
+ _handleAuthStateChange(user) {
168
+ // Reset bindings flag for new auth state
169
+ this._hasUpdatedBindings = false;
170
+
171
+ // Call all registered callbacks
172
+ this._authStateCallbacks.forEach(callback => {
173
+ try {
174
+ callback(user);
175
+ } catch (error) {
176
+ console.error('Auth state callback error:', error);
177
+ }
178
+ });
179
+ }
180
+
181
+ // Get ID token for the current user
182
+ async getIdToken(forceRefresh = false) {
183
+ try {
184
+ const user = this.manager.firebaseAuth.currentUser;
185
+
186
+ const { getIdToken } = await import('firebase/auth');
187
+ return await getIdToken(user, forceRefresh);
188
+ } catch (error) {
189
+ console.error('Get ID token error:', error);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ // Sign in with custom token
195
+ async signInWithCustomToken(token) {
196
+ try {
197
+ if (!this.manager.firebaseAuth) {
198
+ throw new Error('Firebase Auth is not initialized');
199
+ }
200
+
201
+ const { signInWithCustomToken } = await import('firebase/auth');
202
+ const userCredential = await signInWithCustomToken(this.manager.firebaseAuth, token);
203
+ return userCredential.user;
204
+ } catch (error) {
205
+ console.error('Sign in with custom token error:', error);
206
+ throw error;
207
+ }
208
+ }
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
+
226
+ // Sign out the current user
227
+ async signOut() {
228
+ try {
229
+ const { signOut } = await import('firebase/auth');
230
+ await signOut(this.manager.firebaseAuth);
231
+ return true;
232
+ } catch (error) {
233
+ console.error('Sign out error:', error);
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ // Get account data from Firestore
239
+ async _getAccountData(uid) {
240
+ try {
241
+ if (!this.manager.firebaseFirestore) {
242
+ return null;
243
+ }
244
+
245
+ const { doc, getDoc } = await import('firebase/firestore');
246
+
247
+ const accountDoc = doc(this.manager.firebaseFirestore, 'users', uid);
248
+ const snapshot = await getDoc(accountDoc);
249
+
250
+ // Get current Firebase user to pass uid and email to resolver
251
+ const firebaseUser = this.manager.firebaseAuth?.currentUser || { uid };
252
+
253
+ if (snapshot.exists()) {
254
+ // Resolve the account data to ensure proper structure and defaults
255
+ const rawData = snapshot.data();
256
+ const resolvedAccount = resolveAccount(rawData, firebaseUser);
257
+ return resolvedAccount;
258
+ }
259
+
260
+ // If no account exists, return resolved empty object for consistent structure
261
+ return resolveAccount({}, firebaseUser);
262
+ } catch (error) {
263
+ console.error('Get account data error:', error);
264
+ return null;
265
+ }
266
+ }
267
+
268
+ // Set up DOM event listeners for auth buttons
269
+ setupEventListeners() {
270
+ // Only set up once DOM is ready
271
+ if (typeof document === 'undefined') return;
272
+
273
+ // Set up sign out button listeners using event delegation
274
+ document.addEventListener('click', async (event) => {
275
+ // Use closest to handle clicks on child elements
276
+ const signOutBtn = event.target.closest('.auth-signout-btn');
277
+
278
+ if (signOutBtn) {
279
+ event.preventDefault();
280
+ event.stopPropagation();
281
+
282
+ try {
283
+ // Show confirmation
284
+ if (!confirm('Are you sure you want to sign out?')) {
285
+ return;
286
+ }
287
+
288
+ // Sign out
289
+ await this.signOut();
290
+
291
+ // Show success notification
292
+ this.manager.utilities().showNotification('Successfully signed out.', 'success');
293
+
294
+ } catch (error) {
295
+ console.error('Sign out error:', error);
296
+ // Show error notification if utilities are available
297
+ this.manager.utilities().showNotification('Failed to sign out. Please try again.', 'danger');
298
+ }
299
+ }
300
+ });
301
+ }
302
+
303
+ }
304
+
305
+ export default Auth;
@@ -0,0 +1,183 @@
1
+ class Bindings {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this._context = {};
5
+ }
6
+
7
+ // Update bindings with new data
8
+ update(data = {}) {
9
+ // Merge new data with existing context
10
+ // Whatever keys are provided will overwrite existing values
11
+ this._context = {
12
+ ...this._context,
13
+ ...data
14
+ };
15
+
16
+ this._updateBindings(this._context);
17
+ }
18
+
19
+ // Get current context
20
+ getContext() {
21
+ return this._context;
22
+ }
23
+
24
+ // Clear all context
25
+ clear() {
26
+ this._context = {};
27
+ this._updateBindings(this._context);
28
+ }
29
+
30
+ // Main binding update system
31
+ _updateBindings(context) {
32
+ // Find all elements with data-wm-bind attribute
33
+ const bindElements = document.querySelectorAll('[data-wm-bind]');
34
+
35
+ bindElements.forEach(element => {
36
+ const bindValue = element.getAttribute('data-wm-bind');
37
+
38
+ // Parse action and expression
39
+ let action = '@text'; // Default action
40
+ let expression = bindValue;
41
+
42
+ // Check if it starts with an action keyword
43
+ if (bindValue.startsWith('@')) {
44
+ const spaceIndex = bindValue.indexOf(' ');
45
+ if (spaceIndex > -1) {
46
+ action = bindValue.slice(0, spaceIndex);
47
+ expression = bindValue.slice(spaceIndex + 1);
48
+ } else {
49
+ // No space means it's just an action with no expression (like @hide)
50
+ action = bindValue;
51
+ expression = '';
52
+ }
53
+ }
54
+
55
+ // Execute the action
56
+ switch (action) {
57
+ case '@show':
58
+ // Show element if condition is true (or always if no condition)
59
+ const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
60
+ if (shouldShow) {
61
+ element.removeAttribute('hidden');
62
+ } else {
63
+ element.setAttribute('hidden', '');
64
+ }
65
+ break;
66
+
67
+ case '@hide':
68
+ // Hide element if condition is true (or always if no condition)
69
+ const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
70
+ if (shouldHide) {
71
+ element.setAttribute('hidden', '');
72
+ } else {
73
+ element.removeAttribute('hidden');
74
+ }
75
+ break;
76
+
77
+ case '@attr':
78
+ // Set attribute value
79
+ // Format: @attr attributeName expression
80
+ const attrParts = expression.split(' ');
81
+ const attrName = attrParts[0];
82
+ const attrExpression = attrParts.slice(1).join(' ');
83
+ const attrValue = this._resolvePath(context, attrExpression) || '';
84
+
85
+ if (attrValue) {
86
+ element.setAttribute(attrName, attrValue);
87
+ } else {
88
+ element.removeAttribute(attrName);
89
+ }
90
+ break;
91
+
92
+ case '@text':
93
+ default:
94
+ // Set text content (default behavior)
95
+ const value = this._resolvePath(context, expression) || '';
96
+
97
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
98
+ element.value = value;
99
+ } else {
100
+ element.textContent = value;
101
+ }
102
+ break;
103
+
104
+ // Future actions can be added here:
105
+ // case '@class':
106
+ // case '@style':
107
+ }
108
+ });
109
+ }
110
+
111
+ // Resolve nested object path
112
+ _resolvePath(obj, path) {
113
+ if (!obj || !path) return null;
114
+
115
+ return path.split('.').reduce((current, key) => {
116
+ return current?.[key];
117
+ }, obj);
118
+ }
119
+
120
+ // Safely evaluate simple conditions
121
+ _evaluateCondition(condition, context) {
122
+ try {
123
+ // Replace context references with actual values
124
+ // Support: user.field, account.field, simple comparisons
125
+
126
+ // Check for negation operator at the start
127
+ if (condition.trim().startsWith('!')) {
128
+ const expression = condition.trim().slice(1).trim();
129
+ const value = this._resolvePath(context, expression);
130
+ return !value;
131
+ }
132
+
133
+ // Parse the condition to extract left side, operator, and right side
134
+ const comparisonMatch = condition.match(/^(.+?)\s*(===|!==|==|!=|>|<|>=|<=)\s*(.+)$/);
135
+
136
+ if (comparisonMatch) {
137
+ const [, leftPath, operator, rightValue] = comparisonMatch;
138
+
139
+ // Get the left side value
140
+ const leftValue = this._resolvePath(context, leftPath.trim());
141
+
142
+ // Parse the right side (could be string, number, boolean)
143
+ let right = rightValue.trim();
144
+
145
+ // Remove quotes if it's a string
146
+ if ((right.startsWith("'") && right.endsWith("'")) ||
147
+ (right.startsWith('"') && right.endsWith('"'))) {
148
+ right = right.slice(1, -1);
149
+ } else if (right === 'true') {
150
+ right = true;
151
+ } else if (right === 'false') {
152
+ right = false;
153
+ } else if (right === 'null') {
154
+ right = null;
155
+ } else if (!isNaN(right)) {
156
+ right = Number(right);
157
+ }
158
+
159
+ // Evaluate based on operator
160
+ switch (operator) {
161
+ case '===': return leftValue === right;
162
+ case '!==': return leftValue !== right;
163
+ case '==': return leftValue == right;
164
+ case '!=': return leftValue != right;
165
+ case '>': return leftValue > right;
166
+ case '<': return leftValue < right;
167
+ case '>=': return leftValue >= right;
168
+ case '<=': return leftValue <= right;
169
+ default: return false;
170
+ }
171
+ } else {
172
+ // Simple truthy check (e.g., "user.emailVerified" or "account")
173
+ const value = this._resolvePath(context, condition.trim());
174
+ return !!value;
175
+ }
176
+ } catch (error) {
177
+ console.warn('Failed to evaluate condition:', condition, error);
178
+ return false;
179
+ }
180
+ }
181
+ }
182
+
183
+ export default Bindings;
@@ -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
+ }