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,256 @@
1
+ class Auth {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ this._authStateCallbacks = [];
5
+ this._readyCallbacks = [];
6
+ }
7
+
8
+ // Check if user is authenticated
9
+ isAuthenticated() {
10
+ return !!this.getUser();
11
+ }
12
+
13
+ // Get current user
14
+ getUser() {
15
+ const user = this.manager.firebaseAuth?.currentUser;
16
+ if (!user) return null;
17
+
18
+ // Get displayName and photoURL from providerData if not set on main user
19
+ let displayName = user.displayName;
20
+ let photoURL = user.photoURL;
21
+
22
+ // If no displayName or photoURL, check providerData
23
+ if ((!displayName || !photoURL) && user.providerData && user.providerData.length > 0) {
24
+ for (const provider of user.providerData) {
25
+ if (!displayName && provider.displayName) {
26
+ displayName = provider.displayName;
27
+ }
28
+ if (!photoURL && provider.photoURL) {
29
+ photoURL = provider.photoURL;
30
+ }
31
+ // Stop if we found both
32
+ if (displayName && photoURL) break;
33
+ }
34
+ }
35
+
36
+ // If still no displayName, use email or fallback
37
+ if (!displayName) {
38
+ displayName = user.email ? user.email.split('@')[0] : 'User';
39
+ }
40
+
41
+ // If still no photoURL, use a default avatar service
42
+ if (!photoURL) {
43
+ // Use ui-avatars.com which generates avatars from initials
44
+ const name = displayName || user.email.split('@')[0] || 'ME';
45
+ const initials = name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
46
+ photoURL = `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=200&background=random&color=000`;
47
+ }
48
+
49
+ return {
50
+ uid: user.uid,
51
+ email: user.email,
52
+ displayName: displayName,
53
+ photoURL: photoURL,
54
+ emailVerified: user.emailVerified,
55
+ metadata: user.metadata,
56
+ providerData: user.providerData,
57
+ };
58
+ }
59
+
60
+ // Listen for auth state changes (waits for settled state before first callback)
61
+ listen(options = {}, callback) {
62
+ // Handle overloaded signatures - if first param is a function, it's the callback
63
+ if (typeof options === 'function') {
64
+ callback = options;
65
+ options = {};
66
+ }
67
+
68
+ // If Firebase is not enabled, call callback immediately with null
69
+ if (!this.manager.config.firebase?.app?.enabled) {
70
+ callback({ user: null, account: null });
71
+ return () => {}; // Return empty unsubscribe function
72
+ }
73
+
74
+ // Function to get current state and call callback
75
+ const getStateAndCallback = async (user) => {
76
+ // Start with the user which will return null if not authenticated
77
+ const state = { user: this.getUser() };
78
+
79
+ // Then, add account data if requested and user exists
80
+ if (options.account && user && this.manager.firebaseFirestore) {
81
+ try {
82
+ state.account = await this._getAccountData(user.uid);
83
+ } catch (error) {
84
+ state.account = null;
85
+ }
86
+ } else {
87
+ state.account = null;
88
+ }
89
+
90
+ // Update bindings with auth data
91
+ this.manager.bindings().update(state);
92
+
93
+ // Call the provided callback with the state
94
+ callback(state);
95
+ };
96
+
97
+ let hasCalledback = false;
98
+
99
+ // Set up listener for auth state changes
100
+ return this.onAuthStateChanged((user) => {
101
+ // Wait for settled state before first callback
102
+ if (!hasCalledback && !this.manager._firebaseAuthInitialized) {
103
+ return; // Auth state not yet determined
104
+ }
105
+
106
+ hasCalledback = true;
107
+ getStateAndCallback(user);
108
+ });
109
+ }
110
+
111
+ // Listen for auth state changes
112
+ onAuthStateChanged(callback) {
113
+ this._authStateCallbacks.push(callback);
114
+
115
+ // If auth is already initialized, call the callback immediately
116
+ if (this.manager._firebaseAuthInitialized) {
117
+ callback(this.manager.firebaseAuth?.currentUser || null);
118
+ }
119
+
120
+ // Return unsubscribe function
121
+ return () => {
122
+ const index = this._authStateCallbacks.indexOf(callback);
123
+ if (index > -1) {
124
+ this._authStateCallbacks.splice(index, 1);
125
+ }
126
+ };
127
+ }
128
+
129
+ // Internal method to handle auth state changes
130
+ _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
+ });
136
+
137
+ // Call all registered callbacks
138
+ this._authStateCallbacks.forEach(callback => {
139
+ try {
140
+ callback(user);
141
+ } catch (error) {
142
+ console.error('Auth state callback error:', error);
143
+ }
144
+ });
145
+ }
146
+
147
+ // Get ID token for the current user
148
+ async getIdToken(forceRefresh = false) {
149
+ try {
150
+ const user = this.manager.firebaseAuth.currentUser;
151
+
152
+ const { getIdToken } = await import('firebase/auth');
153
+ return await getIdToken(user, forceRefresh);
154
+ } catch (error) {
155
+ console.error('Get ID token error:', error);
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ // Sign in with custom token
161
+ async signInWithCustomToken(token) {
162
+ try {
163
+ if (!this.manager.firebaseAuth) {
164
+ throw new Error('Firebase Auth is not initialized');
165
+ }
166
+
167
+ const { signInWithCustomToken } = await import('firebase/auth');
168
+ const userCredential = await signInWithCustomToken(this.manager.firebaseAuth, token);
169
+ return userCredential.user;
170
+ } catch (error) {
171
+ console.error('Sign in with custom token error:', error);
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ // Sign out the current user
177
+ async signOut() {
178
+ try {
179
+ const { signOut } = await import('firebase/auth');
180
+ await signOut(this.manager.firebaseAuth);
181
+ return true;
182
+ } catch (error) {
183
+ console.error('Sign out error:', error);
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ // Get account data from Firestore
189
+ async _getAccountData(uid) {
190
+ try {
191
+ if (!this.manager.firebaseFirestore) {
192
+ return null;
193
+ }
194
+
195
+ const { doc, getDoc } = await import('firebase/firestore');
196
+ const resolveAccount = (await import('resolve-account')).default;
197
+
198
+ const accountDoc = doc(this.manager.firebaseFirestore, 'users', uid);
199
+ const snapshot = await getDoc(accountDoc);
200
+
201
+ // Get current Firebase user to pass uid and email to resolver
202
+ const firebaseUser = this.manager.firebaseAuth?.currentUser || { uid };
203
+
204
+ if (snapshot.exists()) {
205
+ // Resolve the account data to ensure proper structure and defaults
206
+ const rawData = snapshot.data();
207
+ const resolvedAccount = resolveAccount(rawData, firebaseUser);
208
+ return resolvedAccount;
209
+ }
210
+
211
+ // If no account exists, return resolved empty object for consistent structure
212
+ return resolveAccount({}, firebaseUser);
213
+ } catch (error) {
214
+ console.error('Get account data error:', error);
215
+ return null;
216
+ }
217
+ }
218
+
219
+ // Set up DOM event listeners for auth buttons
220
+ setupEventListeners() {
221
+ // Only set up once DOM is ready
222
+ if (typeof document === 'undefined') return;
223
+
224
+ // Set up sign out button listeners using event delegation
225
+ document.addEventListener('click', async (event) => {
226
+ // Use closest to handle clicks on child elements
227
+ const signOutBtn = event.target.closest('.auth-signout-btn');
228
+
229
+ if (signOutBtn) {
230
+ event.preventDefault();
231
+ event.stopPropagation();
232
+
233
+ try {
234
+ // Show confirmation
235
+ if (!confirm('Are you sure you want to sign out?')) {
236
+ return;
237
+ }
238
+
239
+ // Sign out
240
+ await this.signOut();
241
+
242
+ // Show success notification
243
+ this.manager.utilities().showNotification('Successfully signed out.', 'success');
244
+
245
+ } catch (error) {
246
+ console.error('Sign out error:', error);
247
+ // Show error notification if utilities are available
248
+ this.manager.utilities().showNotification('Failed to sign out. Please try again.', 'danger');
249
+ }
250
+ }
251
+ });
252
+ }
253
+
254
+ }
255
+
256
+ 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,102 @@
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
+ } = options;
19
+
20
+ if (!src) {
21
+ return reject(new Error('Script source is required'));
22
+ }
23
+
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
+ let timeoutId;
31
+ let retryCount = 0;
32
+
33
+ function createAndLoadScript() {
34
+ const script = document.createElement('script');
35
+ script.src = src;
36
+ script.async = async;
37
+ script.defer = defer;
38
+
39
+ if (crossorigin) {
40
+ script.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous';
41
+ }
42
+
43
+ if (integrity) {
44
+ script.integrity = integrity;
45
+ }
46
+
47
+ // Add custom attributes
48
+ attributes.forEach(attr => {
49
+ if (attr.name && attr.value !== undefined) {
50
+ script.setAttribute(attr.name, attr.value);
51
+ }
52
+ });
53
+
54
+ // Set up timeout
55
+ if (timeout > 0) {
56
+ timeoutId = setTimeout(() => {
57
+ script.remove();
58
+ handleError(new Error(`Script load timeout: ${src}`));
59
+ }, timeout);
60
+ }
61
+
62
+ // Event handlers
63
+ script.onload = () => {
64
+ clearTimeout(timeoutId);
65
+ resolve({ script, cached: false });
66
+ };
67
+
68
+ script.onerror = (error) => {
69
+ clearTimeout(timeoutId);
70
+ script.remove();
71
+ handleError(new Error(`Failed to load script: ${src}`));
72
+ };
73
+
74
+ // Append to document
75
+ (document.head || document.documentElement).appendChild(script);
76
+ }
77
+
78
+ function handleError(error) {
79
+ if (retryCount < retries) {
80
+ retryCount++;
81
+ setTimeout(createAndLoadScript, 1000 * retryCount);
82
+ } else {
83
+ reject(error);
84
+ }
85
+ }
86
+
87
+ createAndLoadScript();
88
+ });
89
+ }
90
+
91
+ // Return promise that resolves when DOM is ready
92
+ export function ready() {
93
+ return new Promise((resolve) => {
94
+ if (document.readyState === 'loading') {
95
+ // Wait for DOM if still loading
96
+ document.addEventListener('DOMContentLoaded', resolve);
97
+ } else {
98
+ // DOM is already ready
99
+ resolve();
100
+ }
101
+ });
102
+ }