web-manager 3.2.75 → 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.
- package/CLAUDE.md +42 -0
- package/README.md +63 -0
- package/TODO.md +14 -0
- package/{index.js → _legacy/index.js} +2 -2
- package/_legacy/test/test.js +158 -0
- package/dist/index.js +540 -0
- package/dist/modules/auth.js +256 -0
- package/dist/modules/bindings.js +183 -0
- package/dist/modules/dom.js +102 -0
- package/dist/modules/firestore.js +242 -0
- package/dist/modules/notifications.js +285 -0
- package/dist/modules/sentry.js +166 -0
- package/dist/modules/service-worker.js +321 -0
- package/dist/modules/storage.js +132 -0
- package/dist/modules/utilities.js +143 -0
- package/package.json +9 -8
- /package/{helpers → _legacy/helpers}/auth-pages.js +0 -0
- /package/{lib → _legacy/lib}/account.js +0 -0
- /package/{lib → _legacy/lib}/debug.js +0 -0
- /package/{lib → _legacy/lib}/dom.js +0 -0
- /package/{lib → _legacy/lib}/require.js +0 -0
- /package/{lib → _legacy/lib}/storage.js +0 -0
- /package/{lib → _legacy/lib}/utilities.js +0 -0
@@ -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
|
+
}
|