web-manager 4.3.2 → 4.3.4
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 +3 -0
- package/dist/index.js +11 -1
- package/docs/cdp-debugging.md +29 -0
- package/package.json +7 -1
- package/src/index.js +712 -0
- package/src/modules/analytics.js +250 -0
- package/src/modules/auth.js +420 -0
- package/src/modules/bindings.js +314 -0
- package/src/modules/dom.js +96 -0
- package/src/modules/firestore.js +306 -0
- package/src/modules/notifications.js +391 -0
- package/src/modules/sentry.js +199 -0
- package/src/modules/service-worker.js +196 -0
- package/src/modules/storage.js +133 -0
- package/src/modules/usage.js +264 -0
- package/src/modules/utilities.js +310 -0
- package/CHANGELOG.md +0 -242
- package/TODO.md +0 -59
- package/_legacy/helpers/auth-pages.js +0 -24
- package/_legacy/index.js +0 -1854
- package/_legacy/lib/account.js +0 -666
- package/_legacy/lib/debug.js +0 -56
- package/_legacy/lib/dom.js +0 -351
- package/_legacy/lib/require.js +0 -5
- package/_legacy/lib/storage.js +0 -78
- package/_legacy/lib/utilities.js +0 -180
- package/_legacy/service-worker copy.js +0 -347
- package/_legacy/test/test.js +0 -158
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Dev mode credentials by runtime
|
|
2
|
+
const DEV_CREDENTIALS = {
|
|
3
|
+
'browser-extension': {
|
|
4
|
+
id: 'G-5NWE9SPEEM',
|
|
5
|
+
secret: '33E5W1cCQGKPK4lyMMOWOQ',
|
|
6
|
+
},
|
|
7
|
+
'electron': {
|
|
8
|
+
id: 'G-WMNERKK9J2',
|
|
9
|
+
secret: 'UeKzq8UvS3GD5D2aHcuZgQ',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Supported runtimes for analytics
|
|
14
|
+
const SUPPORTED_RUNTIMES = ['browser-extension', 'electron'];
|
|
15
|
+
|
|
16
|
+
class Analytics {
|
|
17
|
+
constructor(manager) {
|
|
18
|
+
this.manager = manager;
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
this.devMode = false;
|
|
21
|
+
this.runtime = null;
|
|
22
|
+
this.config = null;
|
|
23
|
+
this.measurementId = null;
|
|
24
|
+
this.secret = null;
|
|
25
|
+
this.clientId = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if runtime is supported
|
|
29
|
+
_isSupported() {
|
|
30
|
+
return SUPPORTED_RUNTIMES.includes(this.runtime);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Initialize analytics
|
|
34
|
+
init(config = {}) {
|
|
35
|
+
// Store config
|
|
36
|
+
this.config = config;
|
|
37
|
+
|
|
38
|
+
// Get runtime
|
|
39
|
+
this.runtime = this.manager.utilities().getRuntime();
|
|
40
|
+
|
|
41
|
+
// TODO: Add web runtime support
|
|
42
|
+
if (!this._isSupported()) {
|
|
43
|
+
console.log(`[Analytics] Runtime "${this.runtime}" not supported yet, skipping`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Skip if already initialized
|
|
48
|
+
if (this.initialized) {
|
|
49
|
+
console.log('[Analytics] Already initialized');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for development mode
|
|
54
|
+
this.devMode = this.manager.isDevelopment();
|
|
55
|
+
|
|
56
|
+
// Get measurement ID and secret (use dev credentials in dev mode)
|
|
57
|
+
if (this.devMode) {
|
|
58
|
+
const devCreds = DEV_CREDENTIALS[this.runtime];
|
|
59
|
+
if (devCreds) {
|
|
60
|
+
this.measurementId = devCreds.id;
|
|
61
|
+
this.secret = devCreds.secret;
|
|
62
|
+
console.log(`[Analytics] Dev mode: using ${this.runtime} dev credentials`);
|
|
63
|
+
} else {
|
|
64
|
+
// No dev credentials for this runtime, use provided config
|
|
65
|
+
this.measurementId = config.measurementId || config.id;
|
|
66
|
+
this.secret = config.secret;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
this.measurementId = config.measurementId || config.id;
|
|
70
|
+
this.secret = config.secret;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip if no measurement ID
|
|
74
|
+
if (!this.measurementId) {
|
|
75
|
+
console.log('[Analytics] No measurement ID provided, skipping initialization');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate or retrieve client ID
|
|
80
|
+
this.clientId = this._getClientId();
|
|
81
|
+
|
|
82
|
+
// Log initialization
|
|
83
|
+
console.log(`[Analytics] Initializing with measurement ID: ${this.measurementId}${this.devMode ? ' (dev mode)' : ''} [${this.runtime}]`);
|
|
84
|
+
|
|
85
|
+
// Mark as initialized
|
|
86
|
+
this.initialized = true;
|
|
87
|
+
|
|
88
|
+
// Send initial pageview
|
|
89
|
+
this.event('page_view');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get or generate client ID
|
|
93
|
+
_getClientId() {
|
|
94
|
+
const storageKey = '_ga_client_id';
|
|
95
|
+
|
|
96
|
+
// Try to get existing client ID
|
|
97
|
+
let clientId = null;
|
|
98
|
+
try {
|
|
99
|
+
clientId = localStorage.getItem(storageKey);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// localStorage not available
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate new client ID if needed
|
|
105
|
+
if (!clientId) {
|
|
106
|
+
clientId = `${Math.random().toString(36).substring(2)}.${Date.now()}`;
|
|
107
|
+
try {
|
|
108
|
+
localStorage.setItem(storageKey, clientId);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// localStorage not available
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return clientId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get page data to include with all events
|
|
118
|
+
_getPageData() {
|
|
119
|
+
return {
|
|
120
|
+
page_path: window.location.pathname,
|
|
121
|
+
page_title: document.title,
|
|
122
|
+
page_location: window.location.href,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Track an event
|
|
127
|
+
event(eventName, params = {}) {
|
|
128
|
+
// TODO: Add web runtime support
|
|
129
|
+
if (!this._isSupported()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!this.initialized) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Merge page data with provided params
|
|
138
|
+
const eventParams = {
|
|
139
|
+
...this._getPageData(),
|
|
140
|
+
...params,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Log event
|
|
144
|
+
console.log(`[Analytics] Event: ${eventName}${this.devMode ? ' (dev mode)' : ''}`, eventParams);
|
|
145
|
+
|
|
146
|
+
// Send via Measurement Protocol (fetch)
|
|
147
|
+
this._sendViaFetch(eventName, eventParams);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Send event via Measurement Protocol (fetch)
|
|
151
|
+
_sendViaFetch(eventName, params = {}) {
|
|
152
|
+
// Measurement Protocol requires api_secret
|
|
153
|
+
if (!this.secret) {
|
|
154
|
+
console.warn('[Analytics] No API secret provided, cannot send via Measurement Protocol');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&api_secret=${this.secret}`;
|
|
159
|
+
|
|
160
|
+
const payload = {
|
|
161
|
+
client_id: this.clientId,
|
|
162
|
+
events: [{
|
|
163
|
+
name: eventName,
|
|
164
|
+
params: {
|
|
165
|
+
...params,
|
|
166
|
+
engagement_time_msec: 100,
|
|
167
|
+
session_id: this._getSessionId(),
|
|
168
|
+
},
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Send via fetch (fire and forget)
|
|
173
|
+
fetch(url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
body: JSON.stringify(payload),
|
|
176
|
+
}).catch((err) => {
|
|
177
|
+
console.warn('[Analytics] Failed to send event:', err);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get or generate session ID
|
|
182
|
+
_getSessionId() {
|
|
183
|
+
const storageKey = '_ga_session_id';
|
|
184
|
+
const sessionTimeout = 30 * 60 * 1000; // 30 minutes
|
|
185
|
+
|
|
186
|
+
let sessionData = null;
|
|
187
|
+
try {
|
|
188
|
+
sessionData = JSON.parse(sessionStorage.getItem(storageKey) || 'null');
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// sessionStorage not available
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
|
|
195
|
+
// Check if session is still valid
|
|
196
|
+
if (sessionData && (now - sessionData.lastActive) < sessionTimeout) {
|
|
197
|
+
sessionData.lastActive = now;
|
|
198
|
+
try {
|
|
199
|
+
sessionStorage.setItem(storageKey, JSON.stringify(sessionData));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// sessionStorage not available
|
|
202
|
+
}
|
|
203
|
+
return sessionData.id;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create new session
|
|
207
|
+
const newSession = {
|
|
208
|
+
id: `${Date.now()}`,
|
|
209
|
+
lastActive: now,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
sessionStorage.setItem(storageKey, JSON.stringify(newSession));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// sessionStorage not available
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return newSession.id;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Set user properties
|
|
222
|
+
setUserProperties(properties = {}) {
|
|
223
|
+
// TODO: Add web runtime support
|
|
224
|
+
if (!this._isSupported()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.initialized) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// TODO: Implement for Measurement Protocol
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set user ID
|
|
236
|
+
setUserId(userId) {
|
|
237
|
+
// TODO: Add web runtime support
|
|
238
|
+
if (!this._isSupported()) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!this.initialized) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// TODO: Implement for Measurement Protocol
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default Analytics;
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
const DEFAULT_ACCOUNT = {
|
|
2
|
+
auth: { uid: null, email: null, temporary: false },
|
|
3
|
+
subscription: {
|
|
4
|
+
product: { id: 'basic', name: 'Basic' },
|
|
5
|
+
status: 'active',
|
|
6
|
+
expires: { timestamp: null, timestampUNIX: null },
|
|
7
|
+
trial: { claimed: false, expires: { timestamp: null, timestampUNIX: null } },
|
|
8
|
+
cancellation: { pending: false, date: { timestamp: null, timestampUNIX: null } },
|
|
9
|
+
payment: {
|
|
10
|
+
processor: null,
|
|
11
|
+
orderId: null,
|
|
12
|
+
resourceId: null,
|
|
13
|
+
frequency: null,
|
|
14
|
+
price: 0,
|
|
15
|
+
startDate: { timestamp: null, timestampUNIX: null },
|
|
16
|
+
updatedBy: {
|
|
17
|
+
event: { name: null, id: null },
|
|
18
|
+
date: { timestamp: null, timestampUNIX: null },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
roles: { admin: false, betaTester: false, developer: false },
|
|
23
|
+
affiliate: { code: null, referrals: [] },
|
|
24
|
+
activity: {
|
|
25
|
+
},
|
|
26
|
+
metadata: {
|
|
27
|
+
created: { timestamp: null, timestampUNIX: null },
|
|
28
|
+
updated: { timestamp: null, timestampUNIX: null },
|
|
29
|
+
},
|
|
30
|
+
api: { clientId: null, privateKey: null },
|
|
31
|
+
usage: {},
|
|
32
|
+
personal: { name: { first: null, last: null } },
|
|
33
|
+
oauth2: {},
|
|
34
|
+
attribution: {
|
|
35
|
+
affiliate: { code: null, timestamp: null, url: null, page: null },
|
|
36
|
+
utm: { tags: {}, timestamp: null, url: null, page: null },
|
|
37
|
+
},
|
|
38
|
+
consent: {
|
|
39
|
+
legal: {
|
|
40
|
+
status: 'revoked',
|
|
41
|
+
grantedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
42
|
+
},
|
|
43
|
+
marketing: {
|
|
44
|
+
status: 'revoked',
|
|
45
|
+
grantedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
46
|
+
revokedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function resolveAccount(rawData, firebaseUser) {
|
|
52
|
+
const user = firebaseUser || {};
|
|
53
|
+
const data = rawData || {};
|
|
54
|
+
|
|
55
|
+
// Deep merge: rawData values take precedence over defaults
|
|
56
|
+
function deepMerge(target, source) {
|
|
57
|
+
const result = { ...target };
|
|
58
|
+
for (const key in source) {
|
|
59
|
+
if (!source.hasOwnProperty(key)) continue;
|
|
60
|
+
if (result[key] === null || result[key] === undefined) {
|
|
61
|
+
result[key] = source[key];
|
|
62
|
+
} else if (typeof result[key] === 'object' && !Array.isArray(result[key])
|
|
63
|
+
&& typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
64
|
+
result[key] = deepMerge(result[key], source[key]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const account = deepMerge(data, DEFAULT_ACCOUNT);
|
|
71
|
+
|
|
72
|
+
// Set auth from firebase user if not already set
|
|
73
|
+
account.auth = account.auth || {};
|
|
74
|
+
account.auth.uid = account.auth.uid || user.uid || null;
|
|
75
|
+
account.auth.email = account.auth.email || user.email || null;
|
|
76
|
+
|
|
77
|
+
return account;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class Auth {
|
|
81
|
+
constructor(manager) {
|
|
82
|
+
this.manager = manager;
|
|
83
|
+
this._authStateCallbacks = [];
|
|
84
|
+
this._hasProcessedStateChange = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if user is authenticated
|
|
88
|
+
isAuthenticated() {
|
|
89
|
+
return !!this.getUser();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get current user
|
|
93
|
+
getUser() {
|
|
94
|
+
const user = this.manager.firebaseAuth?.currentUser;
|
|
95
|
+
if (!user) return null;
|
|
96
|
+
|
|
97
|
+
// Get displayName and photoURL from providerData if not set on main user
|
|
98
|
+
let displayName = user.displayName;
|
|
99
|
+
let photoURL = user.photoURL;
|
|
100
|
+
|
|
101
|
+
// If no displayName or photoURL, check providerData
|
|
102
|
+
if ((!displayName || !photoURL) && user.providerData && user.providerData.length > 0) {
|
|
103
|
+
for (const provider of user.providerData) {
|
|
104
|
+
if (!displayName && provider.displayName) {
|
|
105
|
+
displayName = provider.displayName;
|
|
106
|
+
}
|
|
107
|
+
if (!photoURL && provider.photoURL) {
|
|
108
|
+
photoURL = provider.photoURL;
|
|
109
|
+
}
|
|
110
|
+
// Stop if we found both
|
|
111
|
+
if (displayName && photoURL) break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If still no displayName, use email or fallback
|
|
116
|
+
if (!displayName) {
|
|
117
|
+
displayName = user.email ? user.email.split('@')[0] : 'User';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If still no photoURL, use a default avatar service
|
|
121
|
+
if (!photoURL) {
|
|
122
|
+
// Use ui-avatars.com which generates avatars from initials
|
|
123
|
+
const name = displayName || user.email.split('@')[0] || 'ME';
|
|
124
|
+
const initials = name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
|
125
|
+
photoURL = `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=200&background=random&color=000`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
uid: user.uid,
|
|
130
|
+
email: user.email,
|
|
131
|
+
displayName: displayName,
|
|
132
|
+
photoURL: photoURL,
|
|
133
|
+
emailVerified: user.emailVerified,
|
|
134
|
+
metadata: user.metadata,
|
|
135
|
+
providerData: user.providerData,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Listen for auth state changes (waits for settled state before first callback)
|
|
140
|
+
listen(options = {}, callback) {
|
|
141
|
+
// Handle overloaded signatures - if first param is a function, it's the callback
|
|
142
|
+
if (typeof options === 'function') {
|
|
143
|
+
callback = options;
|
|
144
|
+
options = {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If Firebase isn't configured (no firebaseConfig blob), call callback immediately with null.
|
|
148
|
+
if (!this.manager._resolveFirebaseConfig()) {
|
|
149
|
+
callback({
|
|
150
|
+
user: null,
|
|
151
|
+
account: resolveAccount({}, {}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return () => {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build auth state and call the provided callback
|
|
158
|
+
const run = async (user) => {
|
|
159
|
+
const state = { user: this.getUser() };
|
|
160
|
+
|
|
161
|
+
// Fetch account data if the user is logged in and Firestore is available
|
|
162
|
+
if (user && this.manager.firebaseFirestore) {
|
|
163
|
+
try {
|
|
164
|
+
state.account = await this._getAccountData(user.uid);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.manager.sentry().captureException(new Error('Failed to get account data', { cause: error }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Ensure account is always a resolved object
|
|
171
|
+
state.account = state.account || resolveAccount({}, { uid: user?.uid });
|
|
172
|
+
|
|
173
|
+
// Derive resolved subscription state for bindings and consumers
|
|
174
|
+
state.resolved = this.resolveSubscription(state.account);
|
|
175
|
+
|
|
176
|
+
// Update bindings and storage once per auth state change
|
|
177
|
+
if (!this._hasProcessedStateChange) {
|
|
178
|
+
this.manager.bindings().update({
|
|
179
|
+
auth: state,
|
|
180
|
+
usage: this._resolveUsage(state),
|
|
181
|
+
});
|
|
182
|
+
this.manager.storage().set('auth', state);
|
|
183
|
+
|
|
184
|
+
this._hasProcessedStateChange = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
callback(state);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Once listeners: wait for auth to settle, fire once, done
|
|
191
|
+
if (options.once) {
|
|
192
|
+
this.manager._authReady.then(() => {
|
|
193
|
+
run(this.manager.firebaseAuth?.currentUser || null);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return () => {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Persistent listeners: subscribe to all auth state changes (initial + future)
|
|
200
|
+
// If auth already settled, fire the first callback via the promise to catch up
|
|
201
|
+
const unsubscribe = this._subscribe(run);
|
|
202
|
+
|
|
203
|
+
if (this.manager._firebaseAuthInitialized) {
|
|
204
|
+
this.manager._authReady.then(() => {
|
|
205
|
+
run(this.manager.firebaseAuth?.currentUser || null);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return unsubscribe;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Subscribe to ongoing auth state changes (sign-in, sign-out after initial settle)
|
|
213
|
+
_subscribe(callback) {
|
|
214
|
+
this._authStateCallbacks.push(callback);
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
const index = this._authStateCallbacks.indexOf(callback);
|
|
218
|
+
if (index > -1) {
|
|
219
|
+
this._authStateCallbacks.splice(index, 1);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Called by Manager when Firebase auth state changes
|
|
225
|
+
_handleAuthStateChange(user) {
|
|
226
|
+
// Reset state processing flag for new auth state
|
|
227
|
+
this._hasProcessedStateChange = false;
|
|
228
|
+
|
|
229
|
+
// Call all persistent listener callbacks
|
|
230
|
+
this._authStateCallbacks.forEach(callback => {
|
|
231
|
+
try {
|
|
232
|
+
callback(user);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Auth state callback error:', error);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Resolves calculated subscription fields that require derivation logic
|
|
240
|
+
// Raw data (product.id, status, trial, cancellation) is on account.subscription directly
|
|
241
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
242
|
+
// - plan: the plan ID the user effectively has access to RIGHT NOW ('basic' if cancelled/suspended)
|
|
243
|
+
// - active: user has active access (active, trialing, or cancelling)
|
|
244
|
+
// - trialing: user is in an active trial (backend status is 'active' but trial hasn't expired)
|
|
245
|
+
// - cancelling: cancellation is pending (backend status is 'active' but cancellation.pending is true)
|
|
246
|
+
resolveSubscription(account) {
|
|
247
|
+
const subscription = (account || this.manager.storage().get('auth', {})?.account)?.subscription || {};
|
|
248
|
+
const productId = subscription.product?.id || 'basic';
|
|
249
|
+
|
|
250
|
+
// Derive trial and cancelling states from raw backend data
|
|
251
|
+
let trialing = false;
|
|
252
|
+
let cancelling = false;
|
|
253
|
+
|
|
254
|
+
if (productId !== 'basic' && subscription.status === 'active') {
|
|
255
|
+
trialing = !!(subscription.trial?.claimed
|
|
256
|
+
&& subscription.trial?.expires?.timestampUNIX > Math.floor(Date.now() / 1000));
|
|
257
|
+
cancelling = !trialing && !!subscription.cancellation?.pending;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const active = (productId !== 'basic' && subscription.status === 'active');
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
plan: active ? productId : 'basic',
|
|
264
|
+
active,
|
|
265
|
+
trialing,
|
|
266
|
+
cancelling,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Resolve usage bindings from account data + product limits from config.
|
|
271
|
+
// Returns: { credits: { monthly: 5, limit: 100 }, ... }
|
|
272
|
+
//
|
|
273
|
+
// The product catalog lives at `config.payment.products` (OMEGA canonical
|
|
274
|
+
// shape — matches BEM, UJM, and EM). Each product entry has `{ id, limits: {...} }`.
|
|
275
|
+
_resolveUsage(state) {
|
|
276
|
+
const accountUsage = state.account?.usage || {};
|
|
277
|
+
const productId = state.resolved?.plan || 'basic';
|
|
278
|
+
const products = this.manager.config.payment?.products || [];
|
|
279
|
+
const product = products.find(p => p.id === productId) || {};
|
|
280
|
+
const limits = product.limits || {};
|
|
281
|
+
|
|
282
|
+
// Merge current usage with limits for each feature
|
|
283
|
+
const usage = {};
|
|
284
|
+
const keys = new Set([...Object.keys(accountUsage), ...Object.keys(limits)]);
|
|
285
|
+
|
|
286
|
+
for (const key of keys) {
|
|
287
|
+
usage[key] = {
|
|
288
|
+
...(accountUsage[key] || {}),
|
|
289
|
+
limit: limits[key] || 0,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return usage;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Get ID token for the current user
|
|
297
|
+
async getIdToken(forceRefresh = false) {
|
|
298
|
+
try {
|
|
299
|
+
const user = this.manager.firebaseAuth.currentUser;
|
|
300
|
+
|
|
301
|
+
const { getIdToken } = await import('firebase/auth');
|
|
302
|
+
return await getIdToken(user, forceRefresh);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('Get ID token error:', error);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Sign in with custom token
|
|
310
|
+
async signInWithCustomToken(token) {
|
|
311
|
+
try {
|
|
312
|
+
if (!this.manager.firebaseAuth) {
|
|
313
|
+
throw new Error('Firebase Auth is not initialized');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { signInWithCustomToken } = await import('firebase/auth');
|
|
317
|
+
const userCredential = await signInWithCustomToken(this.manager.firebaseAuth, token);
|
|
318
|
+
return userCredential.user;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('Sign in with custom token error:', error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Sign in with email and password
|
|
326
|
+
async signInWithEmailAndPassword(email, password) {
|
|
327
|
+
try {
|
|
328
|
+
if (!this.manager.firebaseAuth) {
|
|
329
|
+
throw new Error('Firebase Auth is not initialized');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { signInWithEmailAndPassword } = await import('firebase/auth');
|
|
333
|
+
const userCredential = await signInWithEmailAndPassword(this.manager.firebaseAuth, email, password);
|
|
334
|
+
return userCredential.user;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Sign in with email and password error:', error);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Sign out the current user
|
|
342
|
+
async signOut() {
|
|
343
|
+
try {
|
|
344
|
+
const { signOut } = await import('firebase/auth');
|
|
345
|
+
await signOut(this.manager.firebaseAuth);
|
|
346
|
+
return true;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Sign out error:', error);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get account data from Firestore
|
|
354
|
+
async _getAccountData(uid) {
|
|
355
|
+
try {
|
|
356
|
+
if (!this.manager.firebaseFirestore) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { doc, getDoc } = await import('firebase/firestore');
|
|
361
|
+
|
|
362
|
+
const accountDoc = doc(this.manager.firebaseFirestore, 'users', uid);
|
|
363
|
+
const snapshot = await getDoc(accountDoc);
|
|
364
|
+
|
|
365
|
+
// Get current Firebase user to pass uid and email to resolver
|
|
366
|
+
const firebaseUser = this.manager.firebaseAuth?.currentUser || { uid };
|
|
367
|
+
|
|
368
|
+
if (snapshot.exists()) {
|
|
369
|
+
// Resolve the account data to ensure proper structure and defaults
|
|
370
|
+
const rawData = snapshot.data();
|
|
371
|
+
const resolvedAccount = resolveAccount(rawData, firebaseUser);
|
|
372
|
+
return resolvedAccount;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// If no account exists, return resolved empty object for consistent structure
|
|
376
|
+
return resolveAccount({}, firebaseUser);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('Get account data error:', error);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Set up DOM event listeners for auth buttons
|
|
384
|
+
setupEventListeners() {
|
|
385
|
+
// Only set up once DOM is ready
|
|
386
|
+
if (typeof document === 'undefined') return;
|
|
387
|
+
|
|
388
|
+
// Set up sign out button listeners using event delegation
|
|
389
|
+
document.addEventListener('click', async (event) => {
|
|
390
|
+
// Use closest to handle clicks on child elements
|
|
391
|
+
const signOutBtn = event.target.closest('.auth-signout-btn');
|
|
392
|
+
|
|
393
|
+
if (signOutBtn) {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
// Show confirmation
|
|
399
|
+
if (!confirm('Are you sure you want to sign out?')) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sign out
|
|
404
|
+
await this.signOut();
|
|
405
|
+
|
|
406
|
+
// Show success notification
|
|
407
|
+
this.manager.utilities().showNotification('Successfully signed out.', 'success');
|
|
408
|
+
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Sign out error:', error);
|
|
411
|
+
// Show error notification if utilities are available
|
|
412
|
+
this.manager.utilities().showNotification('Failed to sign out. Please try again.', 'danger');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export default Auth;
|