shared-features 0.0.1
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/README.md +200 -0
- package/dist/AdPanel-D0BiV6Xb.cjs +88 -0
- package/dist/AdPanel-D0BiV6Xb.cjs.map +1 -0
- package/dist/AdPanel-RGRBf4ub.js +89 -0
- package/dist/AdPanel-RGRBf4ub.js.map +1 -0
- package/dist/analytics-6shJHRZG.cjs +463 -0
- package/dist/analytics-6shJHRZG.cjs.map +1 -0
- package/dist/analytics-Bbmodnm_.js +442 -0
- package/dist/analytics-Bbmodnm_.js.map +1 -0
- package/dist/components/ads/AdPanel.d.ts +12 -0
- package/dist/components/ads/AdPanel.d.ts.map +1 -0
- package/dist/components/ads/index.d.ts +9 -0
- package/dist/components/ads/index.d.ts.map +1 -0
- package/dist/components/index.cjs +5 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/components/index.js.map +1 -0
- package/dist/firebase/config.d.ts +77 -0
- package/dist/firebase/config.d.ts.map +1 -0
- package/dist/firebase/init.d.ts +55 -0
- package/dist/firebase/init.d.ts.map +1 -0
- package/dist/hooks/index.cjs +6 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useCampaigns.d.ts +59 -0
- package/dist/hooks/useCampaigns.d.ts.map +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/services/analytics.d.ts +32 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/campaigns.d.ts +31 -0
- package/dist/services/campaigns.d.ts.map +1 -0
- package/dist/services/index.cjs +18 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/campaigns.d.ts +378 -0
- package/dist/types/campaigns.d.ts.map +1 -0
- package/dist/types/index.cjs +49 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +49 -0
- package/dist/types/index.js.map +1 -0
- package/dist/useCampaigns-3NxODLLs.js +98 -0
- package/dist/useCampaigns-3NxODLLs.js.map +1 -0
- package/dist/useCampaigns-BNOHpETm.cjs +97 -0
- package/dist/useCampaigns-BNOHpETm.cjs.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
const firestore = require("firebase/firestore");
|
|
25
|
+
const app = require("firebase/app");
|
|
26
|
+
const auth = require("firebase/auth");
|
|
27
|
+
let state = {
|
|
28
|
+
initialized: false,
|
|
29
|
+
config: null,
|
|
30
|
+
deviceId: null
|
|
31
|
+
};
|
|
32
|
+
function getState() {
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
function setState(newState) {
|
|
36
|
+
state = { ...state, ...newState };
|
|
37
|
+
}
|
|
38
|
+
function getConfig() {
|
|
39
|
+
if (!state.initialized || !state.config) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"shared-features has not been initialized. Call initSharedFeatures() first."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return state.config;
|
|
45
|
+
}
|
|
46
|
+
function isInitialized() {
|
|
47
|
+
return state.initialized;
|
|
48
|
+
}
|
|
49
|
+
const SHARED_FEATURES_APP_NAME = "shared-features";
|
|
50
|
+
let firebaseApp = null;
|
|
51
|
+
let firestoreDb = null;
|
|
52
|
+
let firebaseAuth = null;
|
|
53
|
+
function generateDeviceId() {
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
const randomPart = Math.random().toString(36).substring(2, 15);
|
|
56
|
+
return `device_${timestamp}_${randomPart}`;
|
|
57
|
+
}
|
|
58
|
+
async function getOrCreateDeviceId() {
|
|
59
|
+
const STORAGE_KEY = "shared_features_device_id";
|
|
60
|
+
try {
|
|
61
|
+
const { Preferences } = await import("@capacitor/preferences");
|
|
62
|
+
const result = await Preferences.get({ key: STORAGE_KEY });
|
|
63
|
+
if (result.value) {
|
|
64
|
+
return result.value;
|
|
65
|
+
}
|
|
66
|
+
const newId = generateDeviceId();
|
|
67
|
+
await Preferences.set({ key: STORAGE_KEY, value: newId });
|
|
68
|
+
return newId;
|
|
69
|
+
} catch {
|
|
70
|
+
try {
|
|
71
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
72
|
+
if (stored) {
|
|
73
|
+
return stored;
|
|
74
|
+
}
|
|
75
|
+
const newId = generateDeviceId();
|
|
76
|
+
localStorage.setItem(STORAGE_KEY, newId);
|
|
77
|
+
return newId;
|
|
78
|
+
} catch {
|
|
79
|
+
return generateDeviceId();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function initSharedFeatures(config) {
|
|
84
|
+
if (isInitialized() && firebaseApp && firestoreDb && firebaseAuth) {
|
|
85
|
+
const currentConfig = getState().config;
|
|
86
|
+
if (currentConfig && currentConfig.firebaseConfig.projectId === config.firebaseConfig.projectId) {
|
|
87
|
+
return { app: firebaseApp, db: firestoreDb, auth: firebaseAuth };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const existingApps = app.getApps();
|
|
91
|
+
const existingApp = existingApps.find(
|
|
92
|
+
(app2) => app2.name === SHARED_FEATURES_APP_NAME
|
|
93
|
+
);
|
|
94
|
+
if (existingApp) {
|
|
95
|
+
firebaseApp = existingApp;
|
|
96
|
+
} else {
|
|
97
|
+
firebaseApp = app.initializeApp(
|
|
98
|
+
config.firebaseConfig,
|
|
99
|
+
SHARED_FEATURES_APP_NAME
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
firestoreDb = firestore.getFirestore(firebaseApp);
|
|
103
|
+
firebaseAuth = auth.getAuth(firebaseApp);
|
|
104
|
+
const deviceId = await getOrCreateDeviceId();
|
|
105
|
+
setState({
|
|
106
|
+
initialized: true,
|
|
107
|
+
config,
|
|
108
|
+
deviceId
|
|
109
|
+
});
|
|
110
|
+
if (config.debug) {
|
|
111
|
+
console.log("[shared-features] Initialized:", {
|
|
112
|
+
projectId: config.projectId,
|
|
113
|
+
projectName: config.projectName,
|
|
114
|
+
platform: config.platform,
|
|
115
|
+
deviceId
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { app: firebaseApp, db: firestoreDb, auth: firebaseAuth };
|
|
119
|
+
}
|
|
120
|
+
function getSharedFeaturesApp() {
|
|
121
|
+
if (!firebaseApp) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"shared-features has not been initialized. Call initSharedFeatures() first."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return firebaseApp;
|
|
127
|
+
}
|
|
128
|
+
function getSharedFeaturesDb() {
|
|
129
|
+
if (!firestoreDb) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"shared-features has not been initialized. Call initSharedFeatures() first."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return firestoreDb;
|
|
135
|
+
}
|
|
136
|
+
function getSharedFeaturesAuth() {
|
|
137
|
+
if (!firebaseAuth) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"shared-features has not been initialized. Call initSharedFeatures() first."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return firebaseAuth;
|
|
143
|
+
}
|
|
144
|
+
function getDeviceId() {
|
|
145
|
+
return getState().deviceId;
|
|
146
|
+
}
|
|
147
|
+
const COLLECTION_CAMPAIGNS = "zaions_campaigns";
|
|
148
|
+
const COLLECTION_PRODUCTS = "zaions_products";
|
|
149
|
+
let campaignsCache = null;
|
|
150
|
+
const CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
151
|
+
let productsCache = null;
|
|
152
|
+
function isCacheValid() {
|
|
153
|
+
if (!campaignsCache) return false;
|
|
154
|
+
return Date.now() - campaignsCache.timestamp < CACHE_TTL_MS;
|
|
155
|
+
}
|
|
156
|
+
function docToCampaign(docId, data) {
|
|
157
|
+
return {
|
|
158
|
+
id: docId,
|
|
159
|
+
productId: data.productId,
|
|
160
|
+
name: data.name,
|
|
161
|
+
status: data.status,
|
|
162
|
+
targetPlatforms: data.targetPlatforms,
|
|
163
|
+
targetAudience: data.targetAudience,
|
|
164
|
+
targetProjects: data.targetProjects || [],
|
|
165
|
+
excludeProductUsers: data.excludeProductUsers,
|
|
166
|
+
placements: data.placements,
|
|
167
|
+
priority: data.priority,
|
|
168
|
+
frequencyDays: data.frequencyDays,
|
|
169
|
+
maxImpressions: data.maxImpressions,
|
|
170
|
+
startDate: data.startDate,
|
|
171
|
+
endDate: data.endDate,
|
|
172
|
+
variant: data.variant,
|
|
173
|
+
customTitle: data.customTitle,
|
|
174
|
+
customTagline: data.customTagline,
|
|
175
|
+
customCta: data.customCta,
|
|
176
|
+
customCtaUrl: data.customCtaUrl,
|
|
177
|
+
customDescription: data.customDescription,
|
|
178
|
+
customProductColor: data.customProductColor,
|
|
179
|
+
customIcon: data.customIcon,
|
|
180
|
+
customFeatures: data.customFeatures,
|
|
181
|
+
totalImpressions: data.totalImpressions,
|
|
182
|
+
totalClicks: data.totalClicks,
|
|
183
|
+
totalCloses: data.totalCloses,
|
|
184
|
+
createdAt: data.createdAt,
|
|
185
|
+
updatedAt: data.updatedAt,
|
|
186
|
+
createdBy: data.createdBy,
|
|
187
|
+
updatedBy: data.updatedBy
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function docToProduct(docId, data) {
|
|
191
|
+
return {
|
|
192
|
+
id: docId,
|
|
193
|
+
name: data.name,
|
|
194
|
+
tagline: data.tagline,
|
|
195
|
+
description: data.description,
|
|
196
|
+
type: data.type,
|
|
197
|
+
url: data.url,
|
|
198
|
+
color: data.color,
|
|
199
|
+
features: data.features || [],
|
|
200
|
+
icon64: data.icon64,
|
|
201
|
+
icon128: data.icon128,
|
|
202
|
+
chromeStoreUrl: data.chromeStoreUrl,
|
|
203
|
+
playStoreUrl: data.playStoreUrl,
|
|
204
|
+
appStoreUrl: data.appStoreUrl,
|
|
205
|
+
webUrl: data.webUrl,
|
|
206
|
+
enabled: data.enabled,
|
|
207
|
+
createdAt: data.createdAt,
|
|
208
|
+
updatedAt: data.updatedAt
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function fetchProducts() {
|
|
212
|
+
const db = getSharedFeaturesDb();
|
|
213
|
+
const snapshot = await firestore.getDocs(firestore.collection(db, COLLECTION_PRODUCTS));
|
|
214
|
+
const products = snapshot.docs.map((d) => docToProduct(d.id, d.data()));
|
|
215
|
+
productsCache = new Map(products.map((p) => [p.id, p]));
|
|
216
|
+
return products;
|
|
217
|
+
}
|
|
218
|
+
async function getProductById(productId) {
|
|
219
|
+
if (productsCache?.has(productId)) {
|
|
220
|
+
return productsCache.get(productId) || null;
|
|
221
|
+
}
|
|
222
|
+
const db = getSharedFeaturesDb();
|
|
223
|
+
const docSnap = await firestore.getDoc(firestore.doc(db, COLLECTION_PRODUCTS, productId));
|
|
224
|
+
if (!docSnap.exists()) return null;
|
|
225
|
+
const product = docToProduct(docSnap.id, docSnap.data());
|
|
226
|
+
if (!productsCache) productsCache = /* @__PURE__ */ new Map();
|
|
227
|
+
productsCache.set(productId, product);
|
|
228
|
+
return product;
|
|
229
|
+
}
|
|
230
|
+
async function fetchCampaigns(options = {}) {
|
|
231
|
+
const config = getConfig();
|
|
232
|
+
const db = getSharedFeaturesDb();
|
|
233
|
+
if (!options.placement && !options.status && !options.productId && isCacheValid()) {
|
|
234
|
+
return campaignsCache.data;
|
|
235
|
+
}
|
|
236
|
+
let q = firestore.query(firestore.collection(db, COLLECTION_CAMPAIGNS));
|
|
237
|
+
if (options.status) {
|
|
238
|
+
q = firestore.query(q, firestore.where("status", "==", options.status));
|
|
239
|
+
}
|
|
240
|
+
if (options.productId) {
|
|
241
|
+
q = firestore.query(q, firestore.where("productId", "==", options.productId));
|
|
242
|
+
}
|
|
243
|
+
q = firestore.query(q, firestore.orderBy("priority", "desc"));
|
|
244
|
+
if (options.limit) {
|
|
245
|
+
q = firestore.query(q, firestore.limit(options.limit));
|
|
246
|
+
}
|
|
247
|
+
const snapshot = await firestore.getDocs(q);
|
|
248
|
+
let campaigns = snapshot.docs.map((d) => docToCampaign(d.id, d.data()));
|
|
249
|
+
if (options.placement) {
|
|
250
|
+
campaigns = campaigns.filter(
|
|
251
|
+
(c) => c.placements.includes(options.placement)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
campaigns = campaigns.filter(
|
|
255
|
+
(c) => c.targetPlatforms.includes(config.platform) || c.targetPlatforms.length === 0
|
|
256
|
+
);
|
|
257
|
+
campaigns = campaigns.filter(
|
|
258
|
+
(c) => c.targetProjects.length === 0 || c.targetProjects.includes(config.projectId)
|
|
259
|
+
);
|
|
260
|
+
if (!options.placement && !options.status && !options.productId) {
|
|
261
|
+
campaignsCache = {
|
|
262
|
+
data: campaigns,
|
|
263
|
+
timestamp: Date.now()
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return campaigns;
|
|
267
|
+
}
|
|
268
|
+
async function fetchActiveCampaigns(placement) {
|
|
269
|
+
const now = firestore.Timestamp.now();
|
|
270
|
+
const campaigns = await fetchCampaigns({ status: "active" });
|
|
271
|
+
if (!productsCache || productsCache.size === 0) {
|
|
272
|
+
await fetchProducts();
|
|
273
|
+
}
|
|
274
|
+
const eligible = campaigns.filter((c) => {
|
|
275
|
+
if (!c.placements.includes(placement)) return false;
|
|
276
|
+
if (c.startDate.toMillis() > now.toMillis()) return false;
|
|
277
|
+
if (c.endDate && c.endDate.toMillis() < now.toMillis()) return false;
|
|
278
|
+
if (c.maxImpressions !== null && c.totalImpressions >= c.maxImpressions)
|
|
279
|
+
return false;
|
|
280
|
+
return true;
|
|
281
|
+
});
|
|
282
|
+
const result = [];
|
|
283
|
+
for (const campaign of eligible) {
|
|
284
|
+
const product = await getProductById(campaign.productId);
|
|
285
|
+
if (product && product.enabled) {
|
|
286
|
+
result.push({ ...campaign, product });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
async function getCampaignById(campaignId) {
|
|
292
|
+
const db = getSharedFeaturesDb();
|
|
293
|
+
const docSnap = await firestore.getDoc(firestore.doc(db, COLLECTION_CAMPAIGNS, campaignId));
|
|
294
|
+
if (!docSnap.exists()) return null;
|
|
295
|
+
return docToCampaign(docSnap.id, docSnap.data());
|
|
296
|
+
}
|
|
297
|
+
function clearCampaignsCache() {
|
|
298
|
+
campaignsCache = null;
|
|
299
|
+
}
|
|
300
|
+
function clearProductsCache() {
|
|
301
|
+
productsCache = null;
|
|
302
|
+
}
|
|
303
|
+
const COLLECTION_IMPRESSIONS = "zaions_impressions";
|
|
304
|
+
const LOCAL_STORAGE_KEY = "shared_features_ad_history";
|
|
305
|
+
async function getLocalAdHistory() {
|
|
306
|
+
try {
|
|
307
|
+
const { Preferences } = await import("@capacitor/preferences");
|
|
308
|
+
const result = await Preferences.get({ key: LOCAL_STORAGE_KEY });
|
|
309
|
+
if (result.value) {
|
|
310
|
+
return JSON.parse(result.value);
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
try {
|
|
314
|
+
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
315
|
+
if (stored) {
|
|
316
|
+
return JSON.parse(stored);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
async function saveLocalAdHistory(history) {
|
|
324
|
+
const serialized = JSON.stringify(history);
|
|
325
|
+
try {
|
|
326
|
+
const { Preferences } = await import("@capacitor/preferences");
|
|
327
|
+
await Preferences.set({ key: LOCAL_STORAGE_KEY, value: serialized });
|
|
328
|
+
} catch {
|
|
329
|
+
try {
|
|
330
|
+
localStorage.setItem(LOCAL_STORAGE_KEY, serialized);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function updateLocalHistory(campaignId, productId, action, frequencyDays) {
|
|
336
|
+
const history = await getLocalAdHistory();
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const nextEligibleAt = now + frequencyDays * 24 * 60 * 60 * 1e3;
|
|
339
|
+
const existing = history[campaignId];
|
|
340
|
+
if (existing) {
|
|
341
|
+
history[campaignId] = {
|
|
342
|
+
...existing,
|
|
343
|
+
lastSeenAt: now,
|
|
344
|
+
impressionCount: action === "impression" ? existing.impressionCount + 1 : existing.impressionCount,
|
|
345
|
+
clicked: existing.clicked || action === "click",
|
|
346
|
+
closed: existing.closed || action === "close",
|
|
347
|
+
nextEligibleAt: action === "impression" ? nextEligibleAt : existing.nextEligibleAt
|
|
348
|
+
};
|
|
349
|
+
} else {
|
|
350
|
+
history[campaignId] = {
|
|
351
|
+
campaignId,
|
|
352
|
+
productId,
|
|
353
|
+
lastSeenAt: now,
|
|
354
|
+
impressionCount: action === "impression" ? 1 : 0,
|
|
355
|
+
clicked: action === "click",
|
|
356
|
+
closed: action === "close",
|
|
357
|
+
nextEligibleAt
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
await saveLocalAdHistory(history);
|
|
361
|
+
}
|
|
362
|
+
async function isEligibleForCampaign(campaignId, _frequencyDays = 20) {
|
|
363
|
+
const history = await getLocalAdHistory();
|
|
364
|
+
const entry = history[campaignId];
|
|
365
|
+
if (!entry) return true;
|
|
366
|
+
return Date.now() >= entry.nextEligibleAt;
|
|
367
|
+
}
|
|
368
|
+
async function getEligibleCampaignIds() {
|
|
369
|
+
const history = await getLocalAdHistory();
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
return Object.entries(history).filter(([, entry]) => now >= entry.nextEligibleAt).map(([campaignId]) => campaignId);
|
|
372
|
+
}
|
|
373
|
+
async function getCampaignHistory(campaignId) {
|
|
374
|
+
const history = await getLocalAdHistory();
|
|
375
|
+
return history[campaignId] || null;
|
|
376
|
+
}
|
|
377
|
+
async function recordImpression(input, frequencyDays = 20) {
|
|
378
|
+
const config = getConfig();
|
|
379
|
+
const state2 = getState();
|
|
380
|
+
const db = getSharedFeaturesDb();
|
|
381
|
+
const impressionData = {
|
|
382
|
+
campaignId: input.campaignId,
|
|
383
|
+
productId: input.productId,
|
|
384
|
+
projectId: config.projectId,
|
|
385
|
+
userId: null,
|
|
386
|
+
// Consumer projects don't authenticate with aoneahsan.com
|
|
387
|
+
deviceId: state2.deviceId || "unknown",
|
|
388
|
+
platform: config.platform,
|
|
389
|
+
placement: input.placement,
|
|
390
|
+
action: input.action,
|
|
391
|
+
variant: input.variant,
|
|
392
|
+
timestamp: firestore.serverTimestamp(),
|
|
393
|
+
...input.sessionId && { sessionId: input.sessionId }
|
|
394
|
+
};
|
|
395
|
+
try {
|
|
396
|
+
await firestore.addDoc(firestore.collection(db, COLLECTION_IMPRESSIONS), impressionData);
|
|
397
|
+
if (config.debug) {
|
|
398
|
+
console.log("[shared-features] Recorded impression:", impressionData);
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (config.debug) {
|
|
402
|
+
console.error("[shared-features] Failed to record impression:", error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
await updateLocalHistory(
|
|
406
|
+
input.campaignId,
|
|
407
|
+
input.productId,
|
|
408
|
+
input.action,
|
|
409
|
+
frequencyDays
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
async function trackImpression(campaignId, productId, placement, variant, frequencyDays) {
|
|
413
|
+
await recordImpression(
|
|
414
|
+
{
|
|
415
|
+
campaignId,
|
|
416
|
+
productId,
|
|
417
|
+
placement,
|
|
418
|
+
action: "impression",
|
|
419
|
+
variant
|
|
420
|
+
},
|
|
421
|
+
frequencyDays
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
async function trackClick(campaignId, productId, placement, variant) {
|
|
425
|
+
await recordImpression({
|
|
426
|
+
campaignId,
|
|
427
|
+
productId,
|
|
428
|
+
placement,
|
|
429
|
+
action: "click",
|
|
430
|
+
variant
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async function trackClose(campaignId, productId, placement, variant) {
|
|
434
|
+
await recordImpression({
|
|
435
|
+
campaignId,
|
|
436
|
+
productId,
|
|
437
|
+
placement,
|
|
438
|
+
action: "close",
|
|
439
|
+
variant
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
exports.clearCampaignsCache = clearCampaignsCache;
|
|
443
|
+
exports.clearProductsCache = clearProductsCache;
|
|
444
|
+
exports.fetchActiveCampaigns = fetchActiveCampaigns;
|
|
445
|
+
exports.fetchCampaigns = fetchCampaigns;
|
|
446
|
+
exports.fetchProducts = fetchProducts;
|
|
447
|
+
exports.getCampaignById = getCampaignById;
|
|
448
|
+
exports.getCampaignHistory = getCampaignHistory;
|
|
449
|
+
exports.getConfig = getConfig;
|
|
450
|
+
exports.getDeviceId = getDeviceId;
|
|
451
|
+
exports.getEligibleCampaignIds = getEligibleCampaignIds;
|
|
452
|
+
exports.getProductById = getProductById;
|
|
453
|
+
exports.getSharedFeaturesApp = getSharedFeaturesApp;
|
|
454
|
+
exports.getSharedFeaturesAuth = getSharedFeaturesAuth;
|
|
455
|
+
exports.getSharedFeaturesDb = getSharedFeaturesDb;
|
|
456
|
+
exports.initSharedFeatures = initSharedFeatures;
|
|
457
|
+
exports.isEligibleForCampaign = isEligibleForCampaign;
|
|
458
|
+
exports.isInitialized = isInitialized;
|
|
459
|
+
exports.recordImpression = recordImpression;
|
|
460
|
+
exports.trackClick = trackClick;
|
|
461
|
+
exports.trackClose = trackClose;
|
|
462
|
+
exports.trackImpression = trackImpression;
|
|
463
|
+
//# sourceMappingURL=analytics-6shJHRZG.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-6shJHRZG.cjs","sources":["../src/firebase/config.ts","../src/firebase/init.ts","../src/services/campaigns.ts","../src/services/analytics.ts"],"sourcesContent":["/**\n * Firebase Configuration Types\n *\n * Types and interfaces for configuring the shared-features Firebase connection.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\n/**\n * Firebase configuration object\n */\nexport interface FirebaseConfig {\n apiKey: string;\n authDomain: string;\n projectId: string;\n storageBucket?: string;\n messagingSenderId?: string;\n appId?: string;\n measurementId?: string;\n}\n\n/**\n * Target platform for the consumer project\n */\nexport type ConsumerPlatform = 'web' | 'android' | 'ios' | 'extension';\n\n/**\n * Configuration for initializing shared-features\n */\nexport interface SharedFeaturesConfig {\n /**\n * Firebase configuration for aoneahsan.com's Firebase project.\n * This is where all shared data is stored.\n */\n firebaseConfig: FirebaseConfig;\n\n /**\n * Unique identifier for this project (e.g., 'ztools', '2fa-studio')\n */\n projectId: string;\n\n /**\n * Display name for this project (e.g., 'ZTools', '2FA Studio')\n */\n projectName: string;\n\n /**\n * Platform type for targeting\n */\n platform: ConsumerPlatform;\n\n /**\n * Whether to enable debug logging (default: false)\n */\n debug?: boolean;\n}\n\n/**\n * Internal state after initialization\n */\nexport interface SharedFeaturesState {\n /** Whether the package has been initialized */\n initialized: boolean;\n /** The active configuration */\n config: SharedFeaturesConfig | null;\n /** Unique device ID for anonymous tracking */\n deviceId: string | null;\n}\n\n/**\n * Singleton state\n */\nlet state: SharedFeaturesState = {\n initialized: false,\n config: null,\n deviceId: null,\n};\n\n/**\n * Get current state\n */\nexport function getState(): SharedFeaturesState {\n return state;\n}\n\n/**\n * Set state (internal use only)\n */\nexport function setState(newState: Partial<SharedFeaturesState>): void {\n state = { ...state, ...newState };\n}\n\n/**\n * Get configuration (throws if not initialized)\n */\nexport function getConfig(): SharedFeaturesConfig {\n if (!state.initialized || !state.config) {\n throw new Error(\n 'shared-features has not been initialized. Call initSharedFeatures() first.'\n );\n }\n return state.config;\n}\n\n/**\n * Check if initialized\n */\nexport function isInitialized(): boolean {\n return state.initialized;\n}\n","/**\n * Firebase Initialization\n *\n * Initialize a secondary Firebase app for connecting to aoneahsan.com's\n * Firebase project from consumer projects.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\nimport { initializeApp, getApps, FirebaseApp } from 'firebase/app';\nimport { getFirestore, Firestore } from 'firebase/firestore';\nimport { getAuth, Auth } from 'firebase/auth';\nimport {\n SharedFeaturesConfig,\n setState,\n getState,\n isInitialized,\n} from './config';\n\nconst SHARED_FEATURES_APP_NAME = 'shared-features';\n\nlet firebaseApp: FirebaseApp | null = null;\nlet firestoreDb: Firestore | null = null;\nlet firebaseAuth: Auth | null = null;\n\n/**\n * Generate a unique device ID for anonymous tracking\n */\nfunction generateDeviceId(): string {\n const timestamp = Date.now();\n const randomPart = Math.random().toString(36).substring(2, 15);\n return `device_${timestamp}_${randomPart}`;\n}\n\n/**\n * Get or create device ID from storage\n */\nasync function getOrCreateDeviceId(): Promise<string> {\n const STORAGE_KEY = 'shared_features_device_id';\n\n try {\n // Try Capacitor Preferences first\n const { Preferences } = await import('@capacitor/preferences');\n const result = await Preferences.get({ key: STORAGE_KEY });\n if (result.value) {\n return result.value;\n }\n const newId = generateDeviceId();\n await Preferences.set({ key: STORAGE_KEY, value: newId });\n return newId;\n } catch {\n // Fallback to localStorage for web\n try {\n const stored = localStorage.getItem(STORAGE_KEY);\n if (stored) {\n return stored;\n }\n const newId = generateDeviceId();\n localStorage.setItem(STORAGE_KEY, newId);\n return newId;\n } catch {\n // If all storage fails, generate ephemeral ID\n return generateDeviceId();\n }\n }\n}\n\n/**\n * Initialize shared-features with the given configuration.\n *\n * This creates a secondary Firebase app connection to aoneahsan.com's\n * Firebase project, separate from the consumer project's own Firebase.\n *\n * @param config - Configuration object\n * @returns Initialized state object\n *\n * @example\n * ```typescript\n * import { initSharedFeatures } from 'shared-features';\n *\n * const sharedFeatures = initSharedFeatures({\n * firebaseConfig: {\n * apiKey: import.meta.env.VITE_ZAIONS_FIREBASE_API_KEY,\n * authDomain: 'aoneahsan-portfolio.firebaseapp.com',\n * projectId: 'aoneahsan-portfolio',\n * },\n * projectId: 'ztools',\n * projectName: 'ZTools',\n * platform: 'web',\n * });\n * ```\n */\nexport async function initSharedFeatures(\n config: SharedFeaturesConfig\n): Promise<{ app: FirebaseApp; db: Firestore; auth: Auth }> {\n // Return existing instance if already initialized with same config\n if (isInitialized() && firebaseApp && firestoreDb && firebaseAuth) {\n const currentConfig = getState().config;\n if (\n currentConfig &&\n currentConfig.firebaseConfig.projectId === config.firebaseConfig.projectId\n ) {\n return { app: firebaseApp, db: firestoreDb, auth: firebaseAuth };\n }\n }\n\n // Check if app already exists\n const existingApps = getApps();\n const existingApp = existingApps.find(\n (app) => app.name === SHARED_FEATURES_APP_NAME\n );\n\n if (existingApp) {\n firebaseApp = existingApp;\n } else {\n // Initialize new Firebase app with unique name\n firebaseApp = initializeApp(\n config.firebaseConfig,\n SHARED_FEATURES_APP_NAME\n );\n }\n\n // Get Firestore and Auth instances\n firestoreDb = getFirestore(firebaseApp);\n firebaseAuth = getAuth(firebaseApp);\n\n // Get or create device ID\n const deviceId = await getOrCreateDeviceId();\n\n // Update state\n setState({\n initialized: true,\n config,\n deviceId,\n });\n\n if (config.debug) {\n console.log('[shared-features] Initialized:', {\n projectId: config.projectId,\n projectName: config.projectName,\n platform: config.platform,\n deviceId,\n });\n }\n\n return { app: firebaseApp, db: firestoreDb, auth: firebaseAuth };\n}\n\n/**\n * Get the shared-features Firebase app instance.\n * Throws if not initialized.\n */\nexport function getSharedFeaturesApp(): FirebaseApp {\n if (!firebaseApp) {\n throw new Error(\n 'shared-features has not been initialized. Call initSharedFeatures() first.'\n );\n }\n return firebaseApp;\n}\n\n/**\n * Get the shared-features Firestore instance.\n * Throws if not initialized.\n */\nexport function getSharedFeaturesDb(): Firestore {\n if (!firestoreDb) {\n throw new Error(\n 'shared-features has not been initialized. Call initSharedFeatures() first.'\n );\n }\n return firestoreDb;\n}\n\n/**\n * Get the shared-features Auth instance.\n * Throws if not initialized.\n */\nexport function getSharedFeaturesAuth(): Auth {\n if (!firebaseAuth) {\n throw new Error(\n 'shared-features has not been initialized. Call initSharedFeatures() first.'\n );\n }\n return firebaseAuth;\n}\n\n/**\n * Get the device ID for anonymous tracking.\n * Returns null if not initialized.\n */\nexport function getDeviceId(): string | null {\n return getState().deviceId;\n}\n","/**\n * Campaigns Service\n *\n * Handles fetching and displaying advertising campaigns from the\n * centralized aoneahsan.com Firebase backend.\n *\n * Consumer projects only need read access - all campaign management\n * is done through the aoneahsan.com admin panel.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\nimport {\n collection,\n doc,\n getDocs,\n getDoc,\n query,\n where,\n orderBy,\n limit as firestoreLimit,\n Timestamp,\n} from 'firebase/firestore';\nimport { getSharedFeaturesDb } from '../firebase/init';\nimport { getConfig } from '../firebase/config';\nimport type {\n Campaign,\n CampaignWithProduct,\n Product,\n AdPlacement,\n CampaignStatus,\n FetchCampaignsOptions,\n} from '../types/campaigns';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst COLLECTION_CAMPAIGNS = 'zaions_campaigns';\nconst COLLECTION_PRODUCTS = 'zaions_products';\n\n// Cache for campaigns\ninterface CampaignsCache {\n data: Campaign[];\n timestamp: number;\n}\nlet campaignsCache: CampaignsCache | null = null;\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\n// Products cache\nlet productsCache: Map<string, Product> | null = null;\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Check if cache is valid\n */\nfunction isCacheValid(): boolean {\n if (!campaignsCache) return false;\n return Date.now() - campaignsCache.timestamp < CACHE_TTL_MS;\n}\n\n/**\n * Convert Firestore document to Campaign\n */\nfunction docToCampaign(\n docId: string,\n data: Record<string, unknown>\n): Campaign {\n return {\n id: docId,\n productId: data.productId as string,\n name: data.name as string,\n status: data.status as CampaignStatus,\n targetPlatforms: data.targetPlatforms as Campaign['targetPlatforms'],\n targetAudience: data.targetAudience as Campaign['targetAudience'],\n targetProjects: (data.targetProjects as string[]) || [],\n excludeProductUsers: data.excludeProductUsers as boolean,\n placements: data.placements as AdPlacement[],\n priority: data.priority as number,\n frequencyDays: data.frequencyDays as number,\n maxImpressions: data.maxImpressions as number | null,\n startDate: data.startDate as Timestamp,\n endDate: data.endDate as Timestamp | null,\n variant: data.variant as Campaign['variant'],\n customTitle: data.customTitle as string | undefined,\n customTagline: data.customTagline as string | undefined,\n customCta: data.customCta as string | undefined,\n customCtaUrl: data.customCtaUrl as string | undefined,\n customDescription: data.customDescription as string | undefined,\n customProductColor: data.customProductColor as string | undefined,\n customIcon: data.customIcon as string | undefined,\n customFeatures: data.customFeatures as string[] | undefined,\n totalImpressions: data.totalImpressions as number,\n totalClicks: data.totalClicks as number,\n totalCloses: data.totalCloses as number,\n createdAt: data.createdAt as Timestamp,\n updatedAt: data.updatedAt as Timestamp,\n createdBy: data.createdBy as string,\n updatedBy: data.updatedBy as string | undefined,\n };\n}\n\n/**\n * Convert Firestore document to Product\n */\nfunction docToProduct(docId: string, data: Record<string, unknown>): Product {\n return {\n id: docId,\n name: data.name as string,\n tagline: data.tagline as string,\n description: data.description as string,\n type: data.type as Product['type'],\n url: data.url as string,\n color: data.color as string,\n features: (data.features as string[]) || [],\n icon64: data.icon64 as string | undefined,\n icon128: data.icon128 as string | undefined,\n chromeStoreUrl: data.chromeStoreUrl as string | undefined,\n playStoreUrl: data.playStoreUrl as string | undefined,\n appStoreUrl: data.appStoreUrl as string | undefined,\n webUrl: data.webUrl as string | undefined,\n enabled: data.enabled as boolean,\n createdAt: data.createdAt as Timestamp,\n updatedAt: data.updatedAt as Timestamp,\n };\n}\n\n// ============================================================================\n// PRODUCTS\n// ============================================================================\n\n/**\n * Fetch all products from Firestore\n */\nexport async function fetchProducts(): Promise<Product[]> {\n const db = getSharedFeaturesDb();\n const snapshot = await getDocs(collection(db, COLLECTION_PRODUCTS));\n const products = snapshot.docs.map((d) => docToProduct(d.id, d.data()));\n\n // Update cache\n productsCache = new Map(products.map((p) => [p.id, p]));\n\n return products;\n}\n\n/**\n * Get a product by ID (uses cache if available)\n */\nexport async function getProductById(\n productId: string\n): Promise<Product | null> {\n // Check cache first\n if (productsCache?.has(productId)) {\n return productsCache.get(productId) || null;\n }\n\n // Fetch from Firestore\n const db = getSharedFeaturesDb();\n const docSnap = await getDoc(doc(db, COLLECTION_PRODUCTS, productId));\n\n if (!docSnap.exists()) return null;\n\n const product = docToProduct(docSnap.id, docSnap.data());\n\n // Add to cache\n if (!productsCache) productsCache = new Map();\n productsCache.set(productId, product);\n\n return product;\n}\n\n// ============================================================================\n// CAMPAIGNS\n// ============================================================================\n\n/**\n * Fetch campaigns with optional filters\n */\nexport async function fetchCampaigns(\n options: FetchCampaignsOptions = {}\n): Promise<Campaign[]> {\n const config = getConfig();\n const db = getSharedFeaturesDb();\n\n // Check cache first (only for unfiltered queries)\n if (\n !options.placement &&\n !options.status &&\n !options.productId &&\n isCacheValid()\n ) {\n return campaignsCache!.data;\n }\n\n let q = query(collection(db, COLLECTION_CAMPAIGNS));\n\n // Apply filters\n if (options.status) {\n q = query(q, where('status', '==', options.status));\n }\n\n if (options.productId) {\n q = query(q, where('productId', '==', options.productId));\n }\n\n q = query(q, orderBy('priority', 'desc'));\n\n if (options.limit) {\n q = query(q, firestoreLimit(options.limit));\n }\n\n const snapshot = await getDocs(q);\n let campaigns = snapshot.docs.map((d) => docToCampaign(d.id, d.data()));\n\n // Client-side filtering for array fields\n if (options.placement) {\n campaigns = campaigns.filter((c) =>\n c.placements.includes(options.placement!)\n );\n }\n\n // Filter by target platform (from config)\n campaigns = campaigns.filter(\n (c) =>\n c.targetPlatforms.includes(config.platform) ||\n c.targetPlatforms.length === 0\n );\n\n // Filter by target projects (empty = all projects)\n campaigns = campaigns.filter(\n (c) =>\n c.targetProjects.length === 0 ||\n c.targetProjects.includes(config.projectId)\n );\n\n // Cache unfiltered results\n if (!options.placement && !options.status && !options.productId) {\n campaignsCache = {\n data: campaigns,\n timestamp: Date.now(),\n };\n }\n\n return campaigns;\n}\n\n/**\n * Fetch active campaigns for a specific placement\n * Returns campaigns with resolved product data\n */\nexport async function fetchActiveCampaigns(\n placement: AdPlacement\n): Promise<CampaignWithProduct[]> {\n const now = Timestamp.now();\n\n const campaigns = await fetchCampaigns({ status: 'active' });\n\n // Ensure products are loaded\n if (!productsCache || productsCache.size === 0) {\n await fetchProducts();\n }\n\n // Filter by placement and date range\n const eligible = campaigns.filter((c) => {\n // Placement check\n if (!c.placements.includes(placement)) return false;\n\n // Date range check\n if (c.startDate.toMillis() > now.toMillis()) return false;\n if (c.endDate && c.endDate.toMillis() < now.toMillis()) return false;\n\n // Max impressions check\n if (c.maxImpressions !== null && c.totalImpressions >= c.maxImpressions)\n return false;\n\n return true;\n });\n\n // Resolve products\n const result: CampaignWithProduct[] = [];\n\n for (const campaign of eligible) {\n const product = await getProductById(campaign.productId);\n if (product && product.enabled) {\n result.push({ ...campaign, product });\n }\n }\n\n return result;\n}\n\n/**\n * Get a single campaign by ID\n */\nexport async function getCampaignById(\n campaignId: string\n): Promise<Campaign | null> {\n const db = getSharedFeaturesDb();\n const docSnap = await getDoc(doc(db, COLLECTION_CAMPAIGNS, campaignId));\n\n if (!docSnap.exists()) return null;\n return docToCampaign(docSnap.id, docSnap.data());\n}\n\n/**\n * Clear the campaigns cache (useful for manual refresh)\n */\nexport function clearCampaignsCache(): void {\n campaignsCache = null;\n}\n\n/**\n * Clear the products cache\n */\nexport function clearProductsCache(): void {\n productsCache = null;\n}\n","/**\n * Analytics Service\n *\n * Handles recording impressions, clicks, and other ad interactions\n * from consumer projects to the centralized aoneahsan.com Firebase.\n *\n * @author Ahsan Mahmood <aoneahsan@gmail.com>\n */\n\nimport {\n collection,\n addDoc,\n serverTimestamp,\n} from 'firebase/firestore';\nimport { getSharedFeaturesDb } from '../firebase/init';\nimport { getConfig, getState } from '../firebase/config';\nimport type {\n AdAction,\n AdPlacement,\n AdVariant,\n RecordImpressionInput,\n AdHistoryEntry,\n} from '../types/campaigns';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst COLLECTION_IMPRESSIONS = 'zaions_impressions';\nconst LOCAL_STORAGE_KEY = 'shared_features_ad_history';\n\n// ============================================================================\n// LOCAL HISTORY MANAGEMENT\n// ============================================================================\n\n/**\n * Get ad history from local storage\n */\nasync function getLocalAdHistory(): Promise<Record<string, AdHistoryEntry>> {\n try {\n // Try Capacitor Preferences first\n const { Preferences } = await import('@capacitor/preferences');\n const result = await Preferences.get({ key: LOCAL_STORAGE_KEY });\n if (result.value) {\n return JSON.parse(result.value);\n }\n } catch {\n // Fallback to localStorage\n try {\n const stored = localStorage.getItem(LOCAL_STORAGE_KEY);\n if (stored) {\n return JSON.parse(stored);\n }\n } catch {\n // Ignore\n }\n }\n return {};\n}\n\n/**\n * Save ad history to local storage\n */\nasync function saveLocalAdHistory(\n history: Record<string, AdHistoryEntry>\n): Promise<void> {\n const serialized = JSON.stringify(history);\n\n try {\n // Try Capacitor Preferences first\n const { Preferences } = await import('@capacitor/preferences');\n await Preferences.set({ key: LOCAL_STORAGE_KEY, value: serialized });\n } catch {\n // Fallback to localStorage\n try {\n localStorage.setItem(LOCAL_STORAGE_KEY, serialized);\n } catch {\n // Ignore storage errors\n }\n }\n}\n\n/**\n * Update local ad history for a campaign\n */\nasync function updateLocalHistory(\n campaignId: string,\n productId: string,\n action: AdAction,\n frequencyDays: number\n): Promise<void> {\n const history = await getLocalAdHistory();\n const now = Date.now();\n const nextEligibleAt = now + frequencyDays * 24 * 60 * 60 * 1000;\n\n const existing = history[campaignId];\n\n if (existing) {\n history[campaignId] = {\n ...existing,\n lastSeenAt: now,\n impressionCount:\n action === 'impression'\n ? existing.impressionCount + 1\n : existing.impressionCount,\n clicked: existing.clicked || action === 'click',\n closed: existing.closed || action === 'close',\n nextEligibleAt:\n action === 'impression' ? nextEligibleAt : existing.nextEligibleAt,\n };\n } else {\n history[campaignId] = {\n campaignId,\n productId,\n lastSeenAt: now,\n impressionCount: action === 'impression' ? 1 : 0,\n clicked: action === 'click',\n closed: action === 'close',\n nextEligibleAt,\n };\n }\n\n await saveLocalAdHistory(history);\n}\n\n// ============================================================================\n// ELIGIBILITY CHECKING\n// ============================================================================\n\n/**\n * Check if user is eligible to see a campaign (based on frequency capping)\n */\nexport async function isEligibleForCampaign(\n campaignId: string,\n _frequencyDays: number = 20\n): Promise<boolean> {\n // Note: _frequencyDays kept for API consistency; eligibility uses stored nextEligibleAt\n const history = await getLocalAdHistory();\n const entry = history[campaignId];\n\n if (!entry) return true;\n\n return Date.now() >= entry.nextEligibleAt;\n}\n\n/**\n * Get all campaigns the user is currently eligible to see\n */\nexport async function getEligibleCampaignIds(): Promise<string[]> {\n const history = await getLocalAdHistory();\n const now = Date.now();\n\n return Object.entries(history)\n .filter(([, entry]) => now >= entry.nextEligibleAt)\n .map(([campaignId]) => campaignId);\n}\n\n/**\n * Get local ad history for a campaign\n */\nexport async function getCampaignHistory(\n campaignId: string\n): Promise<AdHistoryEntry | null> {\n const history = await getLocalAdHistory();\n return history[campaignId] || null;\n}\n\n// ============================================================================\n// IMPRESSION RECORDING\n// ============================================================================\n\n/**\n * Record an ad impression, click, or close\n *\n * This sends the event to the centralized Firebase and updates local history.\n */\nexport async function recordImpression(\n input: RecordImpressionInput,\n frequencyDays: number = 20\n): Promise<void> {\n const config = getConfig();\n const state = getState();\n const db = getSharedFeaturesDb();\n\n const impressionData = {\n campaignId: input.campaignId,\n productId: input.productId,\n projectId: config.projectId,\n userId: null, // Consumer projects don't authenticate with aoneahsan.com\n deviceId: state.deviceId || 'unknown',\n platform: config.platform,\n placement: input.placement,\n action: input.action,\n variant: input.variant,\n timestamp: serverTimestamp(),\n ...(input.sessionId && { sessionId: input.sessionId }),\n };\n\n try {\n // Record to centralized Firebase\n await addDoc(collection(db, COLLECTION_IMPRESSIONS), impressionData);\n\n if (config.debug) {\n console.log('[shared-features] Recorded impression:', impressionData);\n }\n } catch (error) {\n if (config.debug) {\n console.error('[shared-features] Failed to record impression:', error);\n }\n // Don't throw - we don't want analytics failures to break the UI\n }\n\n // Update local history for frequency capping\n await updateLocalHistory(\n input.campaignId,\n input.productId,\n input.action,\n frequencyDays\n );\n}\n\n/**\n * Convenience function to record an impression\n */\nexport async function trackImpression(\n campaignId: string,\n productId: string,\n placement: AdPlacement,\n variant: AdVariant,\n frequencyDays?: number\n): Promise<void> {\n await recordImpression(\n {\n campaignId,\n productId,\n placement,\n action: 'impression',\n variant,\n },\n frequencyDays\n );\n}\n\n/**\n * Convenience function to record a click\n */\nexport async function trackClick(\n campaignId: string,\n productId: string,\n placement: AdPlacement,\n variant: AdVariant\n): Promise<void> {\n await recordImpression({\n campaignId,\n productId,\n placement,\n action: 'click',\n variant,\n });\n}\n\n/**\n * Convenience function to record a close/dismiss\n */\nexport async function trackClose(\n campaignId: string,\n productId: string,\n placement: AdPlacement,\n variant: AdVariant\n): Promise<void> {\n await recordImpression({\n campaignId,\n productId,\n placement,\n action: 'close',\n variant,\n });\n}\n"],"names":["getApps","app","initializeApp","getFirestore","getAuth","getDocs","collection","getDoc","doc","query","where","orderBy","firestoreLimit","Timestamp","state","serverTimestamp","addDoc"],"mappings":"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"}
|