web-manager 4.1.24 → 4.1.26
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/CHANGELOG.md +5 -0
- package/CLAUDE.md +15 -1
- package/README.md +44 -0
- package/dist/modules/auth.js +3 -0
- package/dist/modules/firestore.js +96 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
---
|
|
18
|
+
## [4.1.25] - 2026-03-13
|
|
19
|
+
### Added
|
|
20
|
+
- Automatically attach resolved subscription state (`state.resolved`) to auth state during processing, making `plan`, `active`, `trialing`, and `cancelling` available to bindings and consumers without manual calls.
|
|
21
|
+
|
|
17
22
|
---
|
|
18
23
|
## [4.1.24] - 2026-03-13
|
|
19
24
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -147,9 +147,23 @@ document.body.addEventListener('click', (e) => {
|
|
|
147
147
|
|
|
148
148
|
### Auth (`auth.js`)
|
|
149
149
|
- **Class**: `Auth`
|
|
150
|
-
- **Key Methods**: `listen(options, callback)`, `isAuthenticated()`, `getUser()`, `signInWithEmailAndPassword()`, `signOut()`, `getIdToken()`
|
|
150
|
+
- **Key Methods**: `listen(options, callback)`, `isAuthenticated()`, `getUser()`, `signInWithEmailAndPassword()`, `signOut()`, `getIdToken()`, `resolveSubscription(account?)`
|
|
151
151
|
- **Bindings**: Updates `auth.user` and `auth.account` context
|
|
152
152
|
|
|
153
|
+
#### resolveSubscription(account?)
|
|
154
|
+
Derives calculated subscription fields from raw account data. Returns only fields that require derivation logic — raw data (product.id, status, trial, cancellation) lives on `account.subscription` directly.
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const resolved = auth.resolveSubscription(account);
|
|
158
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
159
|
+
```
|
|
160
|
+
- `plan`: Effective plan ID the user has access to RIGHT NOW (`'basic'` if cancelled/suspended)
|
|
161
|
+
- `active`: User has active access (active, trialing, or cancelling — all mean the user can use the product)
|
|
162
|
+
- `trialing`: In an active trial (status `'active'` + `trial.claimed` + unexpired `trial.expires`)
|
|
163
|
+
- `cancelling`: Cancellation pending (status `'active'` + `cancellation.pending` + NOT trialing)
|
|
164
|
+
|
|
165
|
+
**Unified with BEM**: The same function exists on `User.resolveSubscription(account)` in backend-manager (`helpers/user.js`) with identical logic and return shape.
|
|
166
|
+
|
|
153
167
|
#### Auth Settler Pattern
|
|
154
168
|
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
169
|
|
package/README.md
CHANGED
|
@@ -360,6 +360,50 @@ On page load, Firebase Auth takes time to restore the user session. The auth set
|
|
|
360
360
|
**HTML Auth Classes**:
|
|
361
361
|
- `.auth-signout-btn` - Sign out button (shows confirmation dialog)
|
|
362
362
|
|
|
363
|
+
**Resolve Subscription State**:
|
|
364
|
+
|
|
365
|
+
Derives calculated subscription fields from raw account data. Returns only fields that require logic — raw data is on `account.subscription` directly.
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const resolved = auth.resolveSubscription(account);
|
|
369
|
+
// Or without an argument (falls back to stored auth state):
|
|
370
|
+
const resolved = auth.resolveSubscription();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
When called without an argument, reads the account from `localStorage` (the last auth state saved by `listen()`). Pass an explicit account when you have one to avoid stale data.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
```javascript
|
|
377
|
+
{
|
|
378
|
+
plan: 'basic', // Effective plan ID right now ('basic' if cancelled/suspended)
|
|
379
|
+
active: true, // Has active access (active, trialing, or cancelling)
|
|
380
|
+
trialing: false, // In an active trial (status 'active' + unexpired trial)
|
|
381
|
+
cancelling: false, // Cancellation pending (status 'active' + cancellation.pending)
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Usage:
|
|
386
|
+
```javascript
|
|
387
|
+
auth.listen({ once: true }, (state) => {
|
|
388
|
+
const resolved = auth.resolveSubscription(state.account);
|
|
389
|
+
|
|
390
|
+
if (!resolved.active) {
|
|
391
|
+
// User is on free plan or subscription ended
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (resolved.trialing) {
|
|
395
|
+
// Show trial banner
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (resolved.cancelling) {
|
|
399
|
+
// Show "your plan will cancel at end of period" notice
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Use resolved.plan for effective plan ID
|
|
403
|
+
const product = products.find(p => p.id === resolved.plan);
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
363
407
|
**⚠️ Auth State Timing**:
|
|
364
408
|
|
|
365
409
|
Methods like `auth.isAuthenticated()`, `auth.getUser()`, and `auth.getIdToken()` read the current state directly — they may return `null` before auth settles.
|
package/dist/modules/auth.js
CHANGED
|
@@ -159,6 +159,9 @@ class Auth {
|
|
|
159
159
|
// Ensure account is always a resolved object
|
|
160
160
|
state.account = state.account || resolveAccount({}, { uid: user?.uid });
|
|
161
161
|
|
|
162
|
+
// Derive resolved subscription state for bindings and consumers
|
|
163
|
+
state.resolved = this.resolveSubscription(state.account);
|
|
164
|
+
|
|
162
165
|
// Update bindings and storage once per auth state change
|
|
163
166
|
if (!this._hasProcessedStateChange) {
|
|
164
167
|
this.manager.bindings().update({ auth: state });
|
|
@@ -26,7 +26,7 @@ class Firestore {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
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');
|
|
29
|
+
const { getFirestore, doc: firestoreDoc, collection: firestoreCollection, getDoc, setDoc, updateDoc, deleteDoc, getDocs, query, where, orderBy, limit, startAt, endAt, onSnapshot } = await import('firebase/firestore');
|
|
30
30
|
|
|
31
31
|
// Store references for later use
|
|
32
32
|
this._firestoreMethods = {
|
|
@@ -43,7 +43,8 @@ class Firestore {
|
|
|
43
43
|
orderBy,
|
|
44
44
|
limit,
|
|
45
45
|
startAt,
|
|
46
|
-
endAt
|
|
46
|
+
endAt,
|
|
47
|
+
onSnapshot,
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
// Initialize Firestore
|
|
@@ -112,6 +113,25 @@ class Firestore {
|
|
|
112
113
|
await self._ensureInitialized();
|
|
113
114
|
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
114
115
|
return await self._firestoreMethods.deleteDoc(docRef);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
onSnapshot(callback, errorCallback) {
|
|
119
|
+
let unsubscribe = function () {};
|
|
120
|
+
|
|
121
|
+
self._ensureInitialized().then(function () {
|
|
122
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
123
|
+
|
|
124
|
+
unsubscribe = self._firestoreMethods.onSnapshot(docRef, function (docSnap) {
|
|
125
|
+
callback({
|
|
126
|
+
exists: () => docSnap.exists(),
|
|
127
|
+
data: () => docSnap.data(),
|
|
128
|
+
id: docSnap.id,
|
|
129
|
+
ref: docRef,
|
|
130
|
+
});
|
|
131
|
+
}, errorCallback);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return function () { unsubscribe(); };
|
|
115
135
|
}
|
|
116
136
|
};
|
|
117
137
|
}
|
|
@@ -192,51 +212,87 @@ class Firestore {
|
|
|
192
212
|
},
|
|
193
213
|
|
|
194
214
|
async get() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Build query constraints
|
|
199
|
-
const queryConstraints = [];
|
|
200
|
-
for (const constraint of constraints) {
|
|
201
|
-
switch (constraint.type) {
|
|
202
|
-
case 'where':
|
|
203
|
-
queryConstraints.push(self._firestoreMethods.where(constraint.field, constraint.operator, constraint.value));
|
|
204
|
-
break;
|
|
205
|
-
case 'orderBy':
|
|
206
|
-
queryConstraints.push(self._firestoreMethods.orderBy(constraint.field, constraint.direction));
|
|
207
|
-
break;
|
|
208
|
-
case 'limit':
|
|
209
|
-
queryConstraints.push(self._firestoreMethods.limit(constraint.count));
|
|
210
|
-
break;
|
|
211
|
-
case 'startAt':
|
|
212
|
-
queryConstraints.push(self._firestoreMethods.startAt(...constraint.values));
|
|
213
|
-
break;
|
|
214
|
-
case 'endAt':
|
|
215
|
-
queryConstraints.push(self._firestoreMethods.endAt(...constraint.values));
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const q = self._firestoreMethods.query(collRef, ...queryConstraints);
|
|
221
|
-
const querySnapshot = await self._firestoreMethods.getDocs(q);
|
|
215
|
+
return self._executeQuery(collectionPath, constraints);
|
|
216
|
+
},
|
|
222
217
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
onSnapshot(callback, errorCallback) {
|
|
219
|
+
let unsubscribe = function () {};
|
|
220
|
+
|
|
221
|
+
self._ensureInitialized().then(function () {
|
|
222
|
+
const collRef = self._firestoreMethods.collection(self._db, collectionPath);
|
|
223
|
+
const queryConstraints = self._buildConstraints(constraints);
|
|
224
|
+
const q = self._firestoreMethods.query(collRef, ...queryConstraints);
|
|
225
|
+
|
|
226
|
+
unsubscribe = self._firestoreMethods.onSnapshot(q, function (querySnapshot) {
|
|
227
|
+
callback({
|
|
228
|
+
docs: querySnapshot.docs.map(doc => ({
|
|
229
|
+
id: doc.id,
|
|
230
|
+
data: () => doc.data(),
|
|
231
|
+
exists: () => doc.exists(),
|
|
232
|
+
ref: doc.ref,
|
|
233
|
+
})),
|
|
234
|
+
size: querySnapshot.size,
|
|
235
|
+
empty: querySnapshot.empty,
|
|
236
|
+
forEach: (cb) => querySnapshot.forEach(cb),
|
|
237
|
+
});
|
|
238
|
+
}, errorCallback);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return function () { unsubscribe(); };
|
|
234
242
|
}
|
|
235
243
|
};
|
|
236
244
|
|
|
237
245
|
return queryBuilder;
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
// Build query constraint objects from constraint descriptors
|
|
249
|
+
_buildConstraints(constraints) {
|
|
250
|
+
const queryConstraints = [];
|
|
251
|
+
|
|
252
|
+
for (const constraint of constraints) {
|
|
253
|
+
switch (constraint.type) {
|
|
254
|
+
case 'where':
|
|
255
|
+
queryConstraints.push(this._firestoreMethods.where(constraint.field, constraint.operator, constraint.value));
|
|
256
|
+
break;
|
|
257
|
+
case 'orderBy':
|
|
258
|
+
queryConstraints.push(this._firestoreMethods.orderBy(constraint.field, constraint.direction));
|
|
259
|
+
break;
|
|
260
|
+
case 'limit':
|
|
261
|
+
queryConstraints.push(this._firestoreMethods.limit(constraint.count));
|
|
262
|
+
break;
|
|
263
|
+
case 'startAt':
|
|
264
|
+
queryConstraints.push(this._firestoreMethods.startAt(...constraint.values));
|
|
265
|
+
break;
|
|
266
|
+
case 'endAt':
|
|
267
|
+
queryConstraints.push(this._firestoreMethods.endAt(...constraint.values));
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return queryConstraints;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Execute a query and return formatted results
|
|
276
|
+
async _executeQuery(collectionPath, constraints) {
|
|
277
|
+
await this._ensureInitialized();
|
|
278
|
+
const collRef = this._firestoreMethods.collection(this._db, collectionPath);
|
|
279
|
+
const queryConstraints = this._buildConstraints(constraints);
|
|
280
|
+
const q = this._firestoreMethods.query(collRef, ...queryConstraints);
|
|
281
|
+
const querySnapshot = await this._firestoreMethods.getDocs(q);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
docs: querySnapshot.docs.map(doc => ({
|
|
285
|
+
id: doc.id,
|
|
286
|
+
data: () => doc.data(),
|
|
287
|
+
exists: () => doc.exists(),
|
|
288
|
+
ref: doc.ref,
|
|
289
|
+
})),
|
|
290
|
+
size: querySnapshot.size,
|
|
291
|
+
empty: querySnapshot.empty,
|
|
292
|
+
forEach: (callback) => querySnapshot.forEach(callback),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
240
296
|
// Helper to generate document IDs (similar to Firebase auto-generated IDs)
|
|
241
297
|
_generateId() {
|
|
242
298
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
package/package.json
CHANGED