web-manager 4.1.8 → 4.1.10
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 +8 -0
- package/README.md +27 -34
- package/TODO.md +3 -0
- package/dist/index.js +10 -3
- package/dist/modules/auth.js +32 -52
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -150,6 +150,14 @@ document.body.addEventListener('click', (e) => {
|
|
|
150
150
|
- **Key Methods**: `listen(options, callback)`, `isAuthenticated()`, `getUser()`, `signInWithEmailAndPassword()`, `signOut()`, `getIdToken()`
|
|
151
151
|
- **Bindings**: Updates `auth.user` and `auth.account` context
|
|
152
152
|
|
|
153
|
+
#### Auth Settler Pattern
|
|
154
|
+
Auth uses a promise-based settler (`_authReady`) that resolves once Firebase's first `onAuthStateChanged` fires — the moment auth state is guaranteed (authenticated user OR null). This eliminates race conditions.
|
|
155
|
+
|
|
156
|
+
- **`once` listeners** (`listen({ once: true }, cb)`): Wait for `_authReady`, fire once, done. No cleanup needed.
|
|
157
|
+
- **Persistent listeners** (`listen({}, cb)`): Subscribe to `_authStateCallbacks`. If auth already settled when registered, catch up via `_authReady.then()`. Otherwise, `_handleAuthStateChange` handles the initial call naturally.
|
|
158
|
+
- **`_hasProcessedStateChange`**: Ensures bindings/storage updates run only once per auth state change across all listeners.
|
|
159
|
+
- **Manager owns the promise**: `_authReady` and `_authReadyResolve` live on the Manager instance. The `onAuthStateChanged` callback in `index.js` resolves it on first fire and sets `_firebaseAuthInitialized = true`.
|
|
160
|
+
|
|
153
161
|
### Bindings (`bindings.js`)
|
|
154
162
|
- **Class**: `Bindings`
|
|
155
163
|
- **Key Methods**: `update(data)`, `getContext()`, `clear()`
|
package/README.md
CHANGED
|
@@ -296,20 +296,24 @@ storage.session.clear();
|
|
|
296
296
|
|
|
297
297
|
### Authentication
|
|
298
298
|
|
|
299
|
-
Firebase Authentication wrapper with
|
|
299
|
+
Firebase Authentication wrapper with a promise-based auth settler:
|
|
300
300
|
|
|
301
301
|
```javascript
|
|
302
302
|
const auth = Manager.auth();
|
|
303
303
|
|
|
304
|
-
// Listen for auth
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
// Listen once — waits for auth to settle, fires exactly once
|
|
305
|
+
auth.listen({ once: true }, (state) => {
|
|
306
|
+
if (state.user) {
|
|
307
|
+
console.log('Logged in:', state.user.email);
|
|
308
|
+
console.log('Account:', state.account);
|
|
309
|
+
} else {
|
|
310
|
+
console.log('Not logged in');
|
|
311
|
+
}
|
|
308
312
|
});
|
|
309
313
|
|
|
310
|
-
//
|
|
311
|
-
auth.listen({
|
|
312
|
-
console.log('
|
|
314
|
+
// Persistent listener — fires on initial settle + every future auth change
|
|
315
|
+
const unsubscribe = auth.listen({}, (state) => {
|
|
316
|
+
console.log('Auth changed:', state.user?.email || 'signed out');
|
|
313
317
|
});
|
|
314
318
|
|
|
315
319
|
// Check authentication status
|
|
@@ -319,11 +323,7 @@ if (auth.isAuthenticated()) {
|
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
// Sign in
|
|
322
|
-
|
|
323
|
-
const user = await auth.signInWithEmailAndPassword('user@example.com', 'password');
|
|
324
|
-
} catch (error) {
|
|
325
|
-
console.error('Sign in failed:', error.message);
|
|
326
|
-
}
|
|
326
|
+
await auth.signInWithEmailAndPassword('user@example.com', 'password');
|
|
327
327
|
|
|
328
328
|
// Sign in with custom token (from backend)
|
|
329
329
|
await auth.signInWithCustomToken('custom-jwt-token');
|
|
@@ -335,10 +335,17 @@ const freshToken = await auth.getIdToken(true); // Force refresh
|
|
|
335
335
|
// Sign out
|
|
336
336
|
await auth.signOut();
|
|
337
337
|
|
|
338
|
-
// Stop
|
|
338
|
+
// Stop persistent listener
|
|
339
339
|
unsubscribe();
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
+
**Auth Settler Design**:
|
|
343
|
+
|
|
344
|
+
On page load, Firebase Auth takes time to restore the user session. The auth settler (`Manager._authReady`) is a promise that resolves once Firebase determines the auth state (user or null). All `listen()` callbacks wait for this settler before firing — consumers never see an intermediate/unknown state.
|
|
345
|
+
|
|
346
|
+
- `{ once: true }` — Waits for the settler promise, calls the callback once, done. No cleanup needed.
|
|
347
|
+
- `{}` (persistent) — Gets the initial settled state via `_handleAuthStateChange`, then fires again on every future sign-in/sign-out.
|
|
348
|
+
|
|
342
349
|
**getUser() returns enhanced user object**:
|
|
343
350
|
```javascript
|
|
344
351
|
{
|
|
@@ -351,38 +358,24 @@ unsubscribe();
|
|
|
351
358
|
```
|
|
352
359
|
|
|
353
360
|
**HTML Auth Classes**:
|
|
354
|
-
Add these classes to elements for automatic auth functionality:
|
|
355
361
|
- `.auth-signout-btn` - Sign out button (shows confirmation dialog)
|
|
356
362
|
|
|
357
363
|
**⚠️ Auth State Timing**:
|
|
358
364
|
|
|
359
|
-
|
|
365
|
+
Methods like `auth.isAuthenticated()`, `auth.getUser()`, and `auth.getIdToken()` read the current state directly — they may return `null` before auth settles.
|
|
360
366
|
|
|
361
|
-
**Problem:**
|
|
362
367
|
```javascript
|
|
363
368
|
// ❌ May fail on page load - auth state not yet determined
|
|
364
|
-
await
|
|
365
|
-
const token = await auth.getIdToken(); // Could throw if currentUser is null
|
|
366
|
-
```
|
|
369
|
+
const token = await auth.getIdToken();
|
|
367
370
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (result.user) {
|
|
373
|
-
const token = await auth.getIdToken(); // Safe - user is authenticated
|
|
371
|
+
// ✅ Wait for auth to settle first
|
|
372
|
+
auth.listen({ once: true }, async (state) => {
|
|
373
|
+
if (state.user) {
|
|
374
|
+
const token = await auth.getIdToken(); // Safe
|
|
374
375
|
}
|
|
375
376
|
});
|
|
376
377
|
```
|
|
377
378
|
|
|
378
|
-
**When this matters:**
|
|
379
|
-
- Pages making authenticated API calls immediately on load
|
|
380
|
-
- OAuth callback pages
|
|
381
|
-
- Deep links requiring authentication
|
|
382
|
-
|
|
383
|
-
**When NOT needed:**
|
|
384
|
-
- User-triggered actions (button clicks) - auth state is always determined by then
|
|
385
|
-
|
|
386
379
|
### Data Binding System
|
|
387
380
|
|
|
388
381
|
Reactive DOM updates with `data-wm-bind` attributes:
|
package/TODO.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,8 +20,12 @@ class Manager {
|
|
|
20
20
|
serviceWorker: null
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Auth settler: resolves when Firebase auth first determines user state
|
|
24
24
|
this._firebaseAuthInitialized = false;
|
|
25
|
+
this._authReadyResolve = null;
|
|
26
|
+
this._authReady = new Promise((resolve) => {
|
|
27
|
+
this._authReadyResolve = resolve;
|
|
28
|
+
});
|
|
25
29
|
|
|
26
30
|
// Initialize modules
|
|
27
31
|
this._storage = new Storage();
|
|
@@ -419,8 +423,11 @@ class Manager {
|
|
|
419
423
|
|
|
420
424
|
// Setup auth state listener
|
|
421
425
|
onAuthStateChanged(this._firebaseAuth, (user) => {
|
|
422
|
-
// Mark auth as initialized
|
|
423
|
-
this._firebaseAuthInitialized
|
|
426
|
+
// Mark auth as initialized and resolve the settler promise on first callback
|
|
427
|
+
if (!this._firebaseAuthInitialized) {
|
|
428
|
+
this._firebaseAuthInitialized = true;
|
|
429
|
+
this._authReadyResolve();
|
|
430
|
+
}
|
|
424
431
|
|
|
425
432
|
// Let auth module handle everything including DOM updates
|
|
426
433
|
this._auth._handleAuthStateChange(user);
|
package/dist/modules/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ const DEFAULT_ACCOUNT = {
|
|
|
4
4
|
product: { id: 'basic', name: 'Basic' },
|
|
5
5
|
status: 'active',
|
|
6
6
|
expires: { timestamp: null, timestampUNIX: null },
|
|
7
|
-
trial: {
|
|
7
|
+
trial: { claimed: false, expires: { timestamp: null, timestampUNIX: null } },
|
|
8
8
|
cancellation: { pending: false, date: { timestamp: null, timestampUNIX: null } },
|
|
9
9
|
payment: {
|
|
10
10
|
processor: null,
|
|
@@ -25,8 +25,12 @@ const DEFAULT_ACCOUNT = {
|
|
|
25
25
|
},
|
|
26
26
|
api: { clientId: null, privateKey: null },
|
|
27
27
|
usage: { requests: { total: 0, period: 0 } },
|
|
28
|
-
personal: { name: { first:
|
|
28
|
+
personal: { name: { first: null, last: null } },
|
|
29
29
|
oauth2: {},
|
|
30
|
+
attribution: {
|
|
31
|
+
affiliate: { code: null, timestamp: null, url: null, page: null },
|
|
32
|
+
utm: { tags: {}, timestamp: null, url: null, page: null },
|
|
33
|
+
},
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
function resolveAccount(rawData, firebaseUser) {
|
|
@@ -62,7 +66,6 @@ class Auth {
|
|
|
62
66
|
constructor(manager) {
|
|
63
67
|
this.manager = manager;
|
|
64
68
|
this._authStateCallbacks = [];
|
|
65
|
-
this._readyCallbacks = [];
|
|
66
69
|
this._hasProcessedStateChange = false;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -128,89 +131,66 @@ class Auth {
|
|
|
128
131
|
|
|
129
132
|
// If Firebase is not enabled, call callback immediately with null
|
|
130
133
|
if (!this.manager.config.firebase?.app?.enabled) {
|
|
131
|
-
// Call callback with null user and empty account
|
|
132
134
|
callback({
|
|
133
135
|
user: null,
|
|
134
|
-
account: resolveAccount({}, {})
|
|
136
|
+
account: resolveAccount({}, {}),
|
|
135
137
|
});
|
|
136
138
|
|
|
137
|
-
// Return empty unsubscribe function
|
|
138
139
|
return () => {};
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
// Start with the user which will return null if not authenticated
|
|
142
|
+
// Build auth state and call the provided callback
|
|
143
|
+
const run = async (user) => {
|
|
144
144
|
const state = { user: this.getUser() };
|
|
145
145
|
|
|
146
|
-
//
|
|
147
|
-
// if (options.account && user && this.manager.firebaseFirestore) {
|
|
148
|
-
// Fetch account if the user is logged in AND Firestore is available
|
|
146
|
+
// Fetch account data if the user is logged in and Firestore is available
|
|
149
147
|
if (user && this.manager.firebaseFirestore) {
|
|
150
148
|
try {
|
|
151
149
|
state.account = await this._getAccountData(user.uid);
|
|
152
150
|
} catch (error) {
|
|
153
|
-
// Pass error to Sentry
|
|
154
151
|
this.manager.sentry().captureException(new Error('Failed to get account data', { cause: error }));
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
//
|
|
155
|
+
// Ensure account is always a resolved object
|
|
159
156
|
state.account = state.account || resolveAccount({}, { uid: user?.uid });
|
|
160
157
|
|
|
161
|
-
//
|
|
162
|
-
// Now ONLY the first listener will process the state change until the next auth state change
|
|
158
|
+
// Update bindings and storage once per auth state change
|
|
163
159
|
if (!this._hasProcessedStateChange) {
|
|
164
|
-
// Run update - nest state under 'auth' key for consistent access
|
|
165
160
|
this.manager.bindings().update({ auth: state });
|
|
166
|
-
|
|
167
|
-
// Save to storage
|
|
168
|
-
const storage = this.manager.storage();
|
|
169
|
-
storage.set('auth', state);
|
|
170
|
-
|
|
171
|
-
// Mark that we've processed this state change
|
|
161
|
+
this.manager.storage().set('auth', state);
|
|
172
162
|
this._hasProcessedStateChange = true;
|
|
173
163
|
}
|
|
174
164
|
|
|
175
|
-
// Call the provided callback with the state
|
|
176
165
|
callback(state);
|
|
177
166
|
};
|
|
178
167
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Wait for settled state before first callback
|
|
185
|
-
if (!hasCalledback && !this.manager._firebaseAuthInitialized) {
|
|
186
|
-
return; // Auth state not yet determined
|
|
187
|
-
}
|
|
168
|
+
// Once listeners: wait for auth to settle, fire once, done
|
|
169
|
+
if (options.once) {
|
|
170
|
+
this.manager._authReady.then(() => {
|
|
171
|
+
run(this.manager.firebaseAuth?.currentUser || null);
|
|
172
|
+
});
|
|
188
173
|
|
|
189
|
-
|
|
190
|
-
|
|
174
|
+
return () => {};
|
|
175
|
+
}
|
|
191
176
|
|
|
192
|
-
|
|
193
|
-
|
|
177
|
+
// Persistent listeners: subscribe to all auth state changes (initial + future)
|
|
178
|
+
// If auth already settled, fire the first callback via the promise to catch up
|
|
179
|
+
const unsubscribe = this._subscribe(run);
|
|
194
180
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
181
|
+
if (this.manager._firebaseAuthInitialized) {
|
|
182
|
+
this.manager._authReady.then(() => {
|
|
183
|
+
run(this.manager.firebaseAuth?.currentUser || null);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
200
186
|
|
|
201
187
|
return unsubscribe;
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
//
|
|
205
|
-
|
|
190
|
+
// Subscribe to ongoing auth state changes (sign-in, sign-out after initial settle)
|
|
191
|
+
_subscribe(callback) {
|
|
206
192
|
this._authStateCallbacks.push(callback);
|
|
207
193
|
|
|
208
|
-
// If auth is already initialized, call the callback immediately
|
|
209
|
-
if (this.manager._firebaseAuthInitialized) {
|
|
210
|
-
callback(this.manager.firebaseAuth?.currentUser || null);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Return unsubscribe function
|
|
214
194
|
return () => {
|
|
215
195
|
const index = this._authStateCallbacks.indexOf(callback);
|
|
216
196
|
if (index > -1) {
|
|
@@ -219,12 +199,12 @@ class Auth {
|
|
|
219
199
|
};
|
|
220
200
|
}
|
|
221
201
|
|
|
222
|
-
//
|
|
202
|
+
// Called by Manager when Firebase auth state changes
|
|
223
203
|
_handleAuthStateChange(user) {
|
|
224
204
|
// Reset state processing flag for new auth state
|
|
225
205
|
this._hasProcessedStateChange = false;
|
|
226
206
|
|
|
227
|
-
// Call all
|
|
207
|
+
// Call all persistent listener callbacks
|
|
228
208
|
this._authStateCallbacks.forEach(callback => {
|
|
229
209
|
try {
|
|
230
210
|
callback(user);
|
package/package.json
CHANGED