@moneybar.online/moneybar 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,3273 @@
1
+ import { MoneyBarConfig, DownloadStatus, MoneyBarEvents, UserContext } from './types.js';
2
+ import { createClient } from '@supabase/supabase-js';
3
+
4
+ /**
5
+ * MoneyBar - The navbar of monetization
6
+ *
7
+ * Fix the 3 money-blocking stages that stop revenue:
8
+ * 1. Forced sign-ins before value → Value-first trials
9
+ * 2. Silent drop-offs → Exit feedback capture
10
+ * 3. Broken payment flows → Seamless Google Auth + payments
11
+ *
12
+ * Features:
13
+ * - Server-side usage tracking (incognito-proof)
14
+ * - Google OAuth authentication
15
+ * - Payment integration with DodoPayments
16
+ * - User feedback collection
17
+ * - Automatic UI management (paywall, auth, status)
18
+ * - User context for personalization
19
+ *
20
+ * @example Simple Usage (Recommended):
21
+ * ```javascript
22
+ * import { MoneyBar } from '@moneybar.online/moneybar';
23
+ *
24
+ * const moneyBar = new MoneyBar({
25
+ * appId: 'my-pdf-app',
26
+ * freeDownloadLimit: 3,
27
+ * supabase: { url: 'https://xyz.supabase.co', anonKey: 'anon_key' },
28
+ * payment: { productId: 'prod_xyz' }
29
+ * });
30
+ *
31
+ * // Single button - handles all monetization automatically
32
+ * moneyBar.attachToButton('#download-btn', (userContext) => {
33
+ * // Your action logic here - with user context for personalization
34
+ * if (userContext.isPremium) {
35
+ * generatePremiumPDF();
36
+ * } else {
37
+ * generateBasicPDF();
38
+ * }
39
+ * });
40
+ * ```
41
+ *
42
+ * @example Multiple Buttons:
43
+ * ```javascript
44
+ * moneyBar.attachToButtons({
45
+ * buttons: [
46
+ * {
47
+ * selector: '#free-btn',
48
+ * downloadCallback: (ctx) => downloadFreeTemplate(ctx),
49
+ * isPremium: false
50
+ * },
51
+ * {
52
+ * selector: '#premium-btn',
53
+ * downloadCallback: (ctx) => downloadPremiumTemplate(ctx),
54
+ * isPremium: true
55
+ * }
56
+ * ]
57
+ * });
58
+ * ```
59
+ */
60
+ export class MoneyBar {
61
+ private config: MoneyBarConfig;
62
+ private supabase: any;
63
+ private currentUser: any = null;
64
+ private userFingerprint: string | null = null;
65
+ private eventListeners: Map<keyof MoneyBarEvents, Function[]> = new Map();
66
+ private supabaseConnectionFailed: boolean = false;
67
+ private paymentConnectionFailed: boolean = false;
68
+ private cachedStatus: DownloadStatus | null = null;
69
+ private statusCacheTime: number = 0;
70
+ private readonly STATUS_CACHE_DURATION = 5000; // 5 seconds cache
71
+ private initializationPromise: Promise<DownloadStatus> | null = null;
72
+ private cachedPremiumStatus: boolean | null = null;
73
+ private premiumStatusCacheTime: number = 0;
74
+
75
+ private mergeConfigs(config?: MoneyBarConfig): MoneyBarConfig {
76
+ // Try to get config from window.APP_CONFIG
77
+ const windowConfig = typeof window !== 'undefined' ? (window as any).APP_CONFIG : null;
78
+
79
+ if (!config && !windowConfig) {
80
+ throw new Error('MoneyBar: No configuration provided. Either pass config to constructor or set window.APP_CONFIG');
81
+ }
82
+
83
+ // If no constructor config provided, use window config entirely
84
+ if (!config) {
85
+ return windowConfig as MoneyBarConfig;
86
+ }
87
+
88
+ // If no window config, use constructor config entirely
89
+ if (!windowConfig) {
90
+ return config;
91
+ }
92
+
93
+ // Merge configs: constructor config takes precedence over window config
94
+ return {
95
+ ...windowConfig,
96
+ ...config
97
+ } as MoneyBarConfig;
98
+ }
99
+
100
+ constructor(config?: MoneyBarConfig) {
101
+ // Auto-load from window.APP_CONFIG if no config provided, or merge configs
102
+ this.config = this.mergeConfigs(config);
103
+ if (this.config.options?.debug) {
104
+ // console.log('🔍 DEBUG: MoneyBar constructor - full config:', JSON.stringify(config, null, 2));
105
+ // console.log('🔍 DEBUG: Email config present:', !!config.email);
106
+ }
107
+ this.validateConfig();
108
+ this.initializeSupabase();
109
+ this.initializePayment();
110
+ this.initializeFingerprint();
111
+ this.setupAuthListener();
112
+ this.checkPaymentStatus();
113
+ }
114
+
115
+ /**
116
+ * Main method to handle download attempts
117
+ * Call this from your download button's click handler
118
+ */
119
+ public async handleDownload(): Promise<void> {
120
+ try {
121
+ // Security check: if Supabase connection failed, disable downloads to prevent localStorage bypass
122
+ if (this.supabaseConnectionFailed) {
123
+ this.config.callbacks?.onError?.(new Error('Server connection required for download verification. Please check your configuration.'));
124
+ return;
125
+ }
126
+
127
+ // Security check: if payment configuration is invalid, disable downloads
128
+ if (this.config.payment && this.paymentConnectionFailed) {
129
+ this.config.callbacks?.onError?.(new Error('Payment configuration error. Please contact support.'));
130
+ return;
131
+ }
132
+
133
+ // Check if user is premium first (if authenticated)
134
+ if (this.currentUser) {
135
+ const isPremium = await this.checkPremiumStatus(this.currentUser.email);
136
+ if (isPremium) {
137
+ this.config.callbacks?.onPremiumDownload();
138
+ return;
139
+ }
140
+ }
141
+
142
+ // Check usage limits (works for both authenticated and anonymous users)
143
+ const status = await this.getDownloadStatus();
144
+
145
+ if (status.currentCount >= status.limit) {
146
+ // Limit reached - show paywall/limit message
147
+ this.config.callbacks?.onLimitReached(status.currentCount, status.limit);
148
+ return;
149
+ }
150
+
151
+ // Increment counter and proceed with action
152
+ await this.incrementDownloadCount();
153
+ this.config.callbacks?.onDownloadAllowed();
154
+
155
+ // Invalidate cache since count changed
156
+ this.cachedStatus = null;
157
+
158
+ // Check if limit reached after increment
159
+ const newStatus = await this.getDownloadStatus();
160
+ this.emit('countChanged', { count: newStatus.currentCount, limit: newStatus.limit });
161
+
162
+ if (newStatus.currentCount >= newStatus.limit) {
163
+ // Show paywall for next time
164
+ setTimeout(() => {
165
+ this.config.callbacks?.onLimitReached(newStatus.currentCount, newStatus.limit);
166
+ }, 100);
167
+ }
168
+
169
+ } catch (error) {
170
+ this.handleError(error as Error);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Get current download status for the user
176
+ */
177
+ public async getDownloadStatus(forceRefresh: boolean = false): Promise<DownloadStatus> {
178
+ console.log(`🔍 [INIT] getDownloadStatus called (forceRefresh: ${forceRefresh}) at ${new Date().toISOString()}`);
179
+
180
+ // Check if we can use cached status
181
+ const cacheAge = this.cachedStatus ? Date.now() - this.statusCacheTime : -1;
182
+ const canUseCache = !forceRefresh && this.cachedStatus && cacheAge < this.STATUS_CACHE_DURATION;
183
+
184
+ if (canUseCache) {
185
+ console.log(`🔍 [INIT] Using cached status (age: ${cacheAge}ms)`);
186
+ return this.cachedStatus!;
187
+ }
188
+
189
+ // If there's already an initialization in progress and it's not forced refresh, wait for it
190
+ if (!forceRefresh && this.initializationPromise) {
191
+ console.log(`🔍 [INIT] Waiting for existing initialization promise`);
192
+ return await this.initializationPromise;
193
+ }
194
+
195
+ console.log(`🔍 [INIT] Starting new status fetch (cache age: ${cacheAge}ms)`);
196
+
197
+ // Create the initialization promise
198
+ this.initializationPromise = this.fetchFreshStatus(forceRefresh);
199
+
200
+ try {
201
+ const result = await this.initializationPromise;
202
+ console.log(`🔍 [INIT] Status fetch completed successfully`);
203
+ return result;
204
+ } finally {
205
+ // Clear the promise after completion
206
+ this.initializationPromise = null;
207
+ }
208
+ }
209
+
210
+ private async fetchFreshStatus(forceRefresh: boolean): Promise<DownloadStatus> {
211
+ try {
212
+ let count = 0;
213
+ let isPremium = false;
214
+
215
+ // Only call API if we really need fresh data
216
+ if (forceRefresh || !this.cachedStatus) {
217
+ // Get download count only if we don't have it cached
218
+ count = await this.getDownloadCount();
219
+ } else {
220
+ // Use cached count if available to avoid unnecessary API call
221
+ count = this.cachedStatus.currentCount;
222
+ }
223
+
224
+ // Check premium status only for authenticated users, and only when needed
225
+ if (this.currentUser) {
226
+ const premiumCacheValid = this.cachedPremiumStatus !== null && (Date.now() - this.premiumStatusCacheTime) < this.STATUS_CACHE_DURATION;
227
+
228
+ if (forceRefresh || !premiumCacheValid) {
229
+ console.log(`🔥 [SUPABASE API] About to call checkPremiumStatus from getDownloadStatus (forceRefresh: ${forceRefresh}, cacheValid: ${premiumCacheValid})`);
230
+ isPremium = await this.checkPremiumStatus(this.currentUser.email);
231
+ // Update cache
232
+ this.cachedPremiumStatus = isPremium;
233
+ this.premiumStatusCacheTime = Date.now();
234
+ } else {
235
+ // Use cached premium status to avoid unnecessary API call
236
+ isPremium = this.cachedPremiumStatus!; // We know it's not null because premiumCacheValid is true
237
+ console.log(`🔍 [CACHE] Using cached premium status from getDownloadStatus: ${isPremium}`);
238
+ }
239
+ } else {
240
+ // Unauthenticated users cannot be premium
241
+ isPremium = false;
242
+ }
243
+
244
+ const status: DownloadStatus = {
245
+ currentCount: count,
246
+ limit: this.config.freeDownloadLimit,
247
+ isPremium,
248
+ isAuthenticated: !!this.currentUser,
249
+ remaining: Math.max(0, this.config.freeDownloadLimit - count)
250
+ };
251
+
252
+ // Cache the status
253
+ this.cachedStatus = status;
254
+ this.statusCacheTime = Date.now();
255
+
256
+ return status;
257
+ } catch (error) {
258
+ this.handleError(error as Error);
259
+ // Return safe defaults on error
260
+ const errorStatus: DownloadStatus = {
261
+ currentCount: 0,
262
+ limit: this.config.freeDownloadLimit,
263
+ isPremium: false,
264
+ isAuthenticated: false,
265
+ remaining: this.config.freeDownloadLimit
266
+ };
267
+
268
+ // Cache error status for short time to avoid repeated failures
269
+ this.cachedStatus = errorStatus;
270
+ this.statusCacheTime = Date.now();
271
+
272
+ return errorStatus;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Creates a UserContext object with current user information
278
+ */
279
+ private async getUserContext(): Promise<UserContext> {
280
+ const status = await this.getDownloadStatus();
281
+
282
+ return {
283
+ isPremium: status.isPremium,
284
+ isAuthenticated: status.isAuthenticated,
285
+ email: this.currentUser?.email,
286
+ name: this.currentUser?.user_metadata?.full_name || this.currentUser?.user_metadata?.name,
287
+ currentCount: status.currentCount,
288
+ remaining: status.remaining,
289
+ limit: status.limit,
290
+ user: this.currentUser
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Manually trigger Google sign-in
296
+ */
297
+ public async signIn(): Promise<void> {
298
+ try {
299
+ const { error } = await this.supabase.auth.signInWithOAuth({
300
+ provider: 'google',
301
+ options: {
302
+ redirectTo: `${window.location.origin}${window.location.pathname}`
303
+ }
304
+ });
305
+ if (error) throw error;
306
+ } catch (error) {
307
+ this.handleError(error as Error);
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Sign out current user
313
+ */
314
+ public async signOut(): Promise<void> {
315
+ try {
316
+ await this.supabase.auth.signOut();
317
+ this.currentUser = null;
318
+ this.emit('authChanged', { user: null, isPremium: false });
319
+ } catch (error) {
320
+ this.handleError(error as Error);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Create payment for premium upgrade
326
+ */
327
+ public async createPayment(): Promise<{ checkout_url: string } | null> {
328
+ if (!this.currentUser) {
329
+ throw new Error('User must be signed in to create payment');
330
+ }
331
+
332
+ if (!this.config.payment) {
333
+ throw new Error('Payment configuration not provided');
334
+ }
335
+
336
+ const requestBody = {
337
+ email: this.currentUser.email,
338
+ product_id: this.config.payment.productId,
339
+ mode: this.config.payment.mode || 'test',
340
+ app_id: this.config.appId,
341
+ return_url: `${window.location.origin}${window.location.pathname}?payment=success`
342
+ };
343
+
344
+ console.log(`🔥 [SUPABASE API] createPayment called for email: ${this.currentUser.email} | Time: ${new Date().toISOString()}`);
345
+
346
+ try {
347
+ const response = await fetch(`${this.config.supabase.url}/functions/v1/create-payment`, {
348
+ method: 'POST',
349
+ headers: {
350
+ 'Content-Type': 'application/json',
351
+ 'Authorization': `Bearer ${this.config.supabase.anonKey}`
352
+ },
353
+ body: JSON.stringify(requestBody)
354
+ });
355
+ console.log(`🔥 [SUPABASE API] createPayment response: ${response.status}`);
356
+
357
+ if (this.config.options?.debug) {
358
+ //console.log('🔍 DEBUG: createPayment response status:', response.status);
359
+ }
360
+
361
+ if (!response.ok) {
362
+ const errorText = await response.text();
363
+ if (this.config.options?.debug) {
364
+ console.error('🔍 DEBUG: createPayment error response:', errorText);
365
+ }
366
+
367
+ let errorData;
368
+ try {
369
+ errorData = JSON.parse(errorText);
370
+ } catch {
371
+ errorData = { error: errorText };
372
+ }
373
+
374
+ // Check if this is a product ID error (invalid config)
375
+ if (errorText.includes('404') || errorText.includes('could not be found')) {
376
+ this.paymentConnectionFailed = true;
377
+ if (this.config.options?.debug) {
378
+ console.warn('Invalid payment product ID detected, disabling downloads');
379
+ }
380
+ // Update UI to show disabled state
381
+ this.updateStatusDisplays();
382
+ }
383
+
384
+ throw new Error(errorData.error || 'Payment creation failed');
385
+ }
386
+
387
+ const result = await response.json();
388
+ if (this.config.options?.debug) {
389
+ //console.log('🔍 DEBUG: createPayment success response:', result);
390
+ }
391
+
392
+ return result;
393
+ } catch (error) {
394
+ this.handleError(error as Error);
395
+ return null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Simplified API: Attach download limiting to a single button
401
+ * Handles all UI logic automatically (paywall, auth, payment flow)
402
+ */
403
+ public attachToButton(selector: string, downloadCallback: (() => void) | ((userContext: UserContext) => void)): void {
404
+ //console.log('🎨 [THEME DEBUG] attachToButton called');
405
+ //console.log('🎨 [THEME DEBUG] Full config:', JSON.stringify(this.config, null, 2));
406
+
407
+ const button = document.querySelector(selector) as HTMLButtonElement;
408
+ if (!button) {
409
+ throw new Error(`Button not found: ${selector}`);
410
+ }
411
+
412
+ // Store original button text (for potential future use)
413
+ // const originalText = button.textContent || 'Download';
414
+
415
+ // Create and inject UI elements
416
+ this.injectBaseStyles();
417
+ this.createTitleBar(); // Always create title bar first
418
+ this.createPaywallModal();
419
+ this.createSuccessModal();
420
+ this.createAuthUI();
421
+ this.createStatusDisplay();
422
+
423
+ // Set up button click handler
424
+ // Mark button as attached by LeadFast
425
+ button.setAttribute('data-leadfast-attached', 'true');
426
+
427
+ button.onclick = async (e) => {
428
+ e.preventDefault();
429
+ await this.handleButtonClick(downloadCallback);
430
+ };
431
+
432
+ // Update UI based on auth state
433
+ this.setupUIUpdates();
434
+ }
435
+
436
+ /**
437
+ * Simplified API: Attach download limiting to multiple buttons
438
+ * Each button can have different download callbacks and premium status
439
+ */
440
+ public attachToButtons(config: {
441
+ buttons: Array<{
442
+ selector: string;
443
+ downloadCallback: (() => void) | ((userContext: UserContext) => void);
444
+ isPremium?: boolean;
445
+ premiumLabel?: string;
446
+ }>;
447
+ }): void {
448
+ this.injectBaseStyles();
449
+ this.createTitleBar(); // Always create title bar first
450
+ this.createPaywallModal();
451
+ this.createSuccessModal();
452
+ this.createAuthUI();
453
+
454
+ config.buttons.forEach(buttonConfig => {
455
+ const button = document.querySelector(buttonConfig.selector) as HTMLButtonElement;
456
+ if (!button) {
457
+ console.warn(`Button not found: ${buttonConfig.selector}`);
458
+ return;
459
+ }
460
+
461
+ // Store original text for potential future use
462
+ // const originalText = button.textContent || 'Download';
463
+
464
+ // Set up click handler - all buttons use the same logic now
465
+ // Mark button as attached by LeadFast
466
+ button.setAttribute('data-leadfast-attached', 'true');
467
+
468
+ button.onclick = async (e) => {
469
+ e.preventDefault();
470
+ await this.handleButtonClick(buttonConfig.downloadCallback);
471
+ };
472
+
473
+ this.setupUIUpdates();
474
+ });
475
+
476
+ // Add global status display
477
+ this.createGlobalStatusDisplay();
478
+ }
479
+
480
+ private async handleButtonClick(downloadCallback: (() => void) | ((userContext: UserContext) => void)): Promise<void> {
481
+ // Prevent rapid clicks/race conditions
482
+ const button = document.querySelector('[data-leadfast-attached="true"]') as HTMLButtonElement;
483
+ if (button && button.disabled) {
484
+ console.log('⚠️ Button click ignored - operation already in progress');
485
+ return;
486
+ }
487
+
488
+ if (button) {
489
+ button.disabled = true;
490
+ button.style.opacity = '0.6';
491
+ }
492
+
493
+ try {
494
+ await this.handleButtonClickInternal(downloadCallback);
495
+ } finally {
496
+ // Re-enable button
497
+ if (button) {
498
+ button.disabled = false;
499
+ button.style.opacity = '1';
500
+ }
501
+ }
502
+ }
503
+
504
+ private async handleButtonClickInternal(downloadCallback: (() => void) | ((userContext: UserContext) => void)): Promise<void> {
505
+ // const clickTime = Date.now();
506
+ // console.log(`🕐 [TIMING] Button click started at: ${new Date(clickTime).toISOString()}`);
507
+
508
+ // Security check: if Supabase connection failed, disable downloads to prevent localStorage bypass
509
+ if (this.supabaseConnectionFailed) {
510
+ // console.log(`🕐 [TIMING] Connection failed check: ${Date.now() - clickTime}ms`);
511
+ this.showConnectionError();
512
+ return;
513
+ }
514
+
515
+ // Security check: if payment configuration is invalid, disable downloads
516
+ if (this.config.payment && this.paymentConnectionFailed) {
517
+ // console.log(`🕐 [TIMING] Payment config failed check: ${Date.now() - clickTime}ms`);
518
+ this.showPaymentConfigError();
519
+ return;
520
+ }
521
+
522
+ // Check cache status
523
+ const cacheValid = this.cachedStatus && (Date.now() - this.statusCacheTime) < this.STATUS_CACHE_DURATION;
524
+ // console.log(`🕐 [CACHE] Cache valid: ${cacheValid}, age: ${this.cachedStatus ? Date.now() - this.statusCacheTime : 'no cache'}ms`);
525
+ // console.log(`🕐 [CACHE] Cached status:`, this.cachedStatus);
526
+
527
+ // const statusStartTime = Date.now();
528
+ // Use cached status if available (since it's already displayed in title bar)
529
+ const status = cacheValid
530
+ ? this.cachedStatus!
531
+ : await this.getDownloadStatus();
532
+ // console.log(`🕐 [TIMING] Status check completed: ${Date.now() - statusStartTime}ms (total: ${Date.now() - clickTime}ms)`);
533
+ // console.log(`🕐 [STATUS] Final status:`, status);
534
+
535
+ // Check if user is premium first (quick check from cached status)
536
+ if (status.isPremium) {
537
+ // console.log(`🕐 [TIMING] Premium user - callback executed: ${Date.now() - clickTime}ms`);
538
+ // Get user context for premium users too
539
+ const userContext = await this.getUserContext();
540
+
541
+ // Check if callback expects user context parameter
542
+ if (downloadCallback.length > 0) {
543
+ // Callback expects user context parameter
544
+ (downloadCallback as (userContext: UserContext) => void)(userContext);
545
+ } else {
546
+ // Legacy callback with no parameters
547
+ (downloadCallback as () => void)();
548
+ }
549
+ return;
550
+ }
551
+
552
+ // Quick limit check from cached status - show paywall immediately if needed
553
+ if (status.currentCount >= status.limit) {
554
+ // console.log(`🕐 [TIMING] Limit reached (${status.currentCount}/${status.limit}) - showing paywall: ${Date.now() - clickTime}ms`);
555
+ // Limit reached - show paywall instantly (no additional API calls)
556
+ // const paywallStartTime = Date.now();
557
+ this.showPaywallInstant(status);
558
+ // console.log(`🕐 [TIMING] Paywall shown: ${Date.now() - paywallStartTime}ms (total: ${Date.now() - clickTime}ms)`);
559
+ return;
560
+ }
561
+
562
+ // console.log(`🕐 [TIMING] Limit not reached (${status.currentCount}/${status.limit}) - proceeding with action`);
563
+ // Allow action and increment counter
564
+ // const incrementStartTime = Date.now();
565
+ // Execute user's function first - only increment count if it succeeds
566
+ try {
567
+ // Check if callback expects user context parameter
568
+ if (downloadCallback.length > 0) {
569
+ // Get user context BEFORE increment for callback
570
+ const userContext = await this.getUserContext();
571
+
572
+ // For non-premium users, simulate the increment so user context matches UI
573
+ if (!userContext.isPremium) {
574
+ userContext.currentCount += 1;
575
+ userContext.remaining = Math.max(0, userContext.limit - userContext.currentCount);
576
+ }
577
+
578
+ // Callback expects user context parameter
579
+ (downloadCallback as (userContext: UserContext) => void)(userContext);
580
+ } else {
581
+ // Legacy callback with no parameters
582
+ (downloadCallback as () => void)();
583
+ }
584
+
585
+ // Double-check limit before increment (race condition protection)
586
+ const freshStatus = await this.getDownloadStatus(true); // Force refresh
587
+ if (freshStatus.currentCount >= freshStatus.limit) {
588
+ console.warn(`⚠️ Limit reached during execution (${freshStatus.currentCount}/${freshStatus.limit}) - canceling increment`);
589
+ // User reached limit while their function was executing
590
+ alert('⚠️ You reached the limit while this action was processing. No attempt was counted.');
591
+ return;
592
+ }
593
+
594
+ // Only increment count after successful execution - with limit check
595
+ const newCount = await this.incrementDownloadCount();
596
+
597
+ // Double-check: if server returned count exceeding limit, something went wrong
598
+ if (newCount > this.config.freeDownloadLimit) {
599
+ console.error(`⚠️ Server returned count (${newCount}) exceeding limit (${this.config.freeDownloadLimit})`);
600
+ // Don't update UI with invalid state
601
+ return;
602
+ }
603
+ // console.log(`🕐 [TIMING] Count incremented: ${Date.now() - incrementStartTime}ms`);
604
+
605
+ // Show success message only if explicitly enabled in config
606
+ if (this.config.successMessage?.enabled === true) {
607
+ const title = this.config.successMessage?.title || 'Action Completed!';
608
+ const message = this.config.successMessage?.message || 'Your action completed successfully!';
609
+ this.showSuccess(title, message);
610
+ }
611
+ } catch (error) {
612
+ console.error('User function failed:', error);
613
+ // Don't increment count on error - user shouldn't lose an attempt
614
+ alert('Action failed. Please check the console for details. No attempt was counted.');
615
+ return; // Exit early, don't update UI
616
+ }
617
+
618
+ // Update UI with fresh status (invalidate cache since count changed)
619
+ // console.log(`🕐 [CACHE] Invalidating cache due to count change (was: ${this.cachedStatus ? 'cached' : 'null'})`);
620
+ this.cachedStatus = null;
621
+ this.updateStatusDisplays();
622
+ // console.log(`🕐 [TIMING] Button click completed: ${Date.now() - clickTime}ms`);
623
+ }
624
+
625
+
626
+ private injectBaseStyles(): void {
627
+ //console.log('🎨 [THEME DEBUG] injectBaseStyles called');
628
+ if (document.getElementById('lead-fast-styles')) {
629
+ //console.log('🎨 [THEME DEBUG] Styles already injected, skipping');
630
+ return;
631
+ }
632
+
633
+ // Inject DaisyUI CDN if theme is specified
634
+ if (this.config.theme?.name) {
635
+ //console.log('🎨 [THEME DEBUG] Theme specified, calling injectDaisyUI');
636
+ this.injectDaisyUI();
637
+ } else {
638
+ //console.log('🎨 [THEME DEBUG] No theme specified in config');
639
+ }
640
+
641
+ const styles = document.createElement('style');
642
+ styles.id = 'lead-fast-styles';
643
+ const primaryColor = this.config.theme?.primaryColor || '#3182ce';
644
+
645
+ styles.textContent = `
646
+ /* User Profile Card - Top Right */
647
+ .lead-fast-profile {
648
+ position: fixed;
649
+ top: 20px;
650
+ left: 50%;
651
+ transform: translateX(-50%);
652
+ z-index: 1000;
653
+ font-family: system-ui, -apple-system, sans-serif;
654
+ width: 90%;
655
+ max-width: 1200px;
656
+ }
657
+
658
+ .lead-fast-title-bar {
659
+ background: rgba(250, 247, 245, 0.9) !important;
660
+ border: 1px solid rgba(231, 226, 223, 0.6) !important;
661
+ color: #291334 !important;
662
+ border-radius: 16px;
663
+ padding: 12px 24px;
664
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.1), 0 2px 8px rgb(0 0 0 / 0.05);
665
+ backdrop-filter: blur(20px);
666
+ display: flex;
667
+ align-items: center;
668
+ justify-content: space-between;
669
+ min-height: 60px;
670
+ font-family: system-ui, -apple-system, sans-serif;
671
+ }
672
+
673
+ /* Theme variations for floating title bar */
674
+ [data-theme="bumblebee"] .lead-fast-title-bar {
675
+ background: rgba(255, 248, 220, 0.9) !important;
676
+ border-color: rgba(254, 215, 170, 0.6) !important;
677
+ color: #1c1917 !important;
678
+ }
679
+
680
+ [data-theme="garden"] .lead-fast-title-bar {
681
+ background: rgba(240, 253, 244, 0.9) !important;
682
+ border-color: rgba(134, 239, 172, 0.6) !important;
683
+ color: #14532d !important;
684
+ }
685
+
686
+ [data-theme="emerald"] .lead-fast-title-bar {
687
+ background: rgba(236, 253, 245, 0.9) !important;
688
+ border-color: rgba(167, 243, 208, 0.6) !important;
689
+ color: #065f46 !important;
690
+ }
691
+
692
+ [data-theme="corporate"] .lead-fast-title-bar {
693
+ background: rgba(248, 250, 252, 0.9) !important;
694
+ border-color: rgba(226, 232, 240, 0.6) !important;
695
+ color: #1e293b !important;
696
+ }
697
+
698
+ [data-theme="dark"] .lead-fast-title-bar {
699
+ background: rgba(31, 41, 55, 0.9) !important;
700
+ border-color: rgba(55, 65, 81, 0.6) !important;
701
+ color: #f9fafb !important;
702
+ }
703
+
704
+ [data-theme="cyberpunk"] .lead-fast-title-bar {
705
+ background: rgba(0, 20, 36, 0.9) !important;
706
+ border-color: rgba(7, 89, 133, 0.6) !important;
707
+ color: #0ea5e9 !important;
708
+ box-shadow: 0 8px 32px rgba(14, 165, 233, 0.2), 0 2px 8px rgba(14, 165, 233, 0.1);
709
+ }
710
+
711
+ [data-theme="valentine"] .lead-fast-title-bar {
712
+ background: rgba(233, 30, 122, 0.15) !important;
713
+ border-color: rgba(233, 30, 122, 0.6) !important;
714
+ color: #831843 !important;
715
+ }
716
+
717
+ [data-theme="synthwave"] .lead-fast-title-bar {
718
+ background: rgba(32, 20, 64, 0.9) !important;
719
+ border-color: rgba(186, 85, 211, 0.6) !important;
720
+ color: #ff00ff !important;
721
+ box-shadow: 0 8px 32px rgba(255, 0, 255, 0.2), 0 2px 8px rgba(255, 0, 255, 0.1);
722
+ }
723
+
724
+ [data-theme="dracula"] .lead-fast-title-bar {
725
+ background: rgba(40, 42, 54, 0.9) !important;
726
+ border-color: rgba(98, 114, 164, 0.6) !important;
727
+ color: #f8f8f2 !important;
728
+ }
729
+
730
+ [data-theme="halloween"] .lead-fast-title-bar {
731
+ background: rgba(26, 26, 26, 0.9) !important;
732
+ border-color: rgba(255, 165, 0, 0.6) !important;
733
+ color: #ff6600 !important;
734
+ }
735
+
736
+ [data-theme="forest"] .lead-fast-title-bar {
737
+ background: rgba(23, 46, 23, 0.9) !important;
738
+ border-color: rgba(34, 197, 94, 0.6) !important;
739
+ color: #22c55e !important;
740
+ }
741
+
742
+ [data-theme="luxury"] .lead-fast-title-bar {
743
+ background: rgba(9, 9, 11, 0.9) !important;
744
+ border-color: rgba(212, 175, 55, 0.6) !important;
745
+ color: #d4af37 !important;
746
+ }
747
+
748
+ [data-theme="night"] .lead-fast-title-bar {
749
+ background: rgba(15, 23, 42, 0.9) !important;
750
+ border-color: rgba(30, 58, 138, 0.6) !important;
751
+ color: #60a5fa !important;
752
+ }
753
+
754
+ [data-theme="light"] .lead-fast-title-bar {
755
+ background: rgba(255, 255, 255, 0.9) !important;
756
+ border-color: rgba(229, 231, 235, 0.6) !important;
757
+ color: #1f2937 !important;
758
+ }
759
+
760
+ [data-theme="cupcake"] .lead-fast-title-bar {
761
+ background: rgba(250, 235, 215, 0.9) !important;
762
+ border-color: rgba(219, 185, 156, 0.6) !important;
763
+ color: #8b4513 !important;
764
+ }
765
+
766
+ [data-theme="retro"] .lead-fast-title-bar {
767
+ background: rgba(212, 165, 116, 0.9) !important;
768
+ border-color: rgba(185, 144, 102, 0.6) !important;
769
+ color: #5d4037 !important;
770
+ }
771
+
772
+ [data-theme="aqua"] .lead-fast-title-bar {
773
+ background: rgba(240, 253, 250, 0.9) !important;
774
+ border-color: rgba(125, 211, 252, 0.6) !important;
775
+ color: #155e75 !important;
776
+ }
777
+
778
+ [data-theme="lofi"] .lead-fast-title-bar {
779
+ background: rgba(248, 248, 248, 0.9) !important;
780
+ border-color: rgba(68, 68, 68, 0.6) !important;
781
+ color: #1a1a1a !important;
782
+ }
783
+
784
+ [data-theme="pastel"] .lead-fast-title-bar {
785
+ background: rgba(254, 251, 255, 0.9) !important;
786
+ border-color: rgba(209, 196, 233, 0.6) !important;
787
+ color: #7c3aed !important;
788
+ }
789
+
790
+ [data-theme="fantasy"] .lead-fast-title-bar {
791
+ background: rgba(255, 247, 237, 0.9) !important;
792
+ border-color: rgba(249, 168, 212, 0.6) !important;
793
+ color: #be185d !important;
794
+ }
795
+
796
+ [data-theme="wireframe"] .lead-fast-title-bar {
797
+ background: rgba(223, 223, 223, 0.9) !important;
798
+ border-color: rgba(0, 0, 0, 0.3) !important;
799
+ color: #000000 !important;
800
+ }
801
+
802
+ [data-theme="black"] .lead-fast-title-bar {
803
+ background: rgba(0, 0, 0, 0.9) !important;
804
+ border-color: rgba(55, 55, 55, 0.6) !important;
805
+ color: #ffffff !important;
806
+ }
807
+
808
+ [data-theme="cmyk"] .lead-fast-title-bar {
809
+ background: rgba(0, 255, 255, 0.9) !important;
810
+ border-color: rgba(255, 0, 255, 0.6) !important;
811
+ color: #000000 !important;
812
+ }
813
+
814
+ [data-theme="autumn"] .lead-fast-title-bar {
815
+ background: rgba(139, 69, 19, 0.9) !important;
816
+ border-color: rgba(255, 140, 0, 0.6) !important;
817
+ color: #ff8c00 !important;
818
+ }
819
+
820
+ [data-theme="business"] .lead-fast-title-bar {
821
+ background: rgba(29, 78, 216, 0.9) !important;
822
+ border-color: rgba(59, 130, 246, 0.6) !important;
823
+ color: #3b82f6 !important;
824
+ }
825
+
826
+ [data-theme="acid"] .lead-fast-title-bar {
827
+ background: rgba(255, 255, 0, 0.9) !important;
828
+ border-color: rgba(255, 0, 255, 0.6) !important;
829
+ color: #000000 !important;
830
+ }
831
+
832
+ [data-theme="lemonade"] .lead-fast-title-bar {
833
+ background: rgba(255, 255, 224, 0.9) !important;
834
+ border-color: rgba(34, 197, 94, 0.6) !important;
835
+ color: #15803d !important;
836
+ }
837
+
838
+ [data-theme="coffee"] .lead-fast-title-bar {
839
+ background: rgba(101, 67, 33, 0.9) !important;
840
+ border-color: rgba(160, 82, 45, 0.6) !important;
841
+ color: #d2b48c !important;
842
+ }
843
+
844
+ [data-theme="winter"] .lead-fast-title-bar {
845
+ background: rgba(248, 250, 252, 0.9) !important;
846
+ border-color: rgba(59, 130, 246, 0.6) !important;
847
+ color: #1e40af !important;
848
+ }
849
+
850
+ .lead-fast-title-section {
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 16px;
854
+ }
855
+
856
+ .lead-fast-logo {
857
+ font-size: 20px;
858
+ font-weight: 700;
859
+ display: flex;
860
+ align-items: center;
861
+ gap: 8px;
862
+ color: inherit;
863
+ text-decoration: none;
864
+ }
865
+
866
+ .lead-fast-logo img {
867
+ width: 32px;
868
+ height: 32px;
869
+ border-radius: 6px;
870
+ }
871
+
872
+ .lead-fast-nav {
873
+ display: flex;
874
+ align-items: center;
875
+ gap: 32px;
876
+ margin-left: 24px;
877
+ }
878
+
879
+ .lead-fast-nav-link {
880
+ color: inherit;
881
+ text-decoration: none;
882
+ font-weight: 500;
883
+ font-size: 14px;
884
+ opacity: 0.8;
885
+ transition: all 0.2s;
886
+ }
887
+
888
+ .lead-fast-nav-link:hover {
889
+ opacity: 1;
890
+ text-decoration: none;
891
+ }
892
+
893
+ .lead-fast-user-section {
894
+ display: flex;
895
+ align-items: center;
896
+ gap: 12px;
897
+ }
898
+
899
+ .lead-fast-avatar {
900
+ width: 36px;
901
+ height: 36px;
902
+ border-radius: 50%;
903
+ background: linear-gradient(135deg, ${primaryColor}, #4f46e5);
904
+ display: flex;
905
+ align-items: center;
906
+ justify-content: center;
907
+ color: white;
908
+ font-weight: 600;
909
+ font-size: 14px;
910
+ }
911
+
912
+ .lead-fast-user-info {
913
+ display: flex;
914
+ flex-direction: column;
915
+ align-items: flex-start;
916
+ gap: 2px;
917
+ }
918
+
919
+ .lead-fast-user-email {
920
+ color: inherit !important;
921
+ font-size: 13px;
922
+ font-weight: 500;
923
+ line-height: 1;
924
+ }
925
+
926
+ .lead-fast-user-status {
927
+ display: flex;
928
+ align-items: center;
929
+ gap: 6px;
930
+ }
931
+
932
+ .lead-fast-badge {
933
+ padding: 2px 6px;
934
+ border-radius: 12px;
935
+ font-size: 10px;
936
+ font-weight: 600;
937
+ text-transform: uppercase;
938
+ letter-spacing: 0.025em;
939
+ line-height: 1;
940
+ }
941
+
942
+ .lead-fast-badge.premium {
943
+ background: #10b981 !important;
944
+ color: #ffffff !important;
945
+ }
946
+
947
+ .lead-fast-badge.free {
948
+ background: #65c3c8 !important;
949
+ color: #291334 !important;
950
+ }
951
+
952
+ [data-theme="dark"] .lead-fast-badge.premium {
953
+ background: #059669 !important;
954
+ color: #ffffff !important;
955
+ }
956
+
957
+ [data-theme="dark"] .lead-fast-badge.free {
958
+ background: #374151 !important;
959
+ color: #f9fafb !important;
960
+ }
961
+
962
+ .lead-fast-count {
963
+ font-size: 10px;
964
+ color: inherit !important;
965
+ opacity: 0.7;
966
+ line-height: 1;
967
+ }
968
+
969
+ .lead-fast-signout {
970
+ background: rgba(0, 0, 0, 0.1) !important;
971
+ border: none;
972
+ color: inherit !important;
973
+ cursor: pointer;
974
+ padding: 6px 12px;
975
+ border-radius: 8px;
976
+ transition: all 0.2s;
977
+ font-size: 11px;
978
+ font-weight: 500;
979
+ margin-left: 8px;
980
+ }
981
+
982
+ [data-theme="dark"] .lead-fast-signout {
983
+ background: rgba(255, 255, 255, 0.1) !important;
984
+ }
985
+
986
+ [data-theme="cyberpunk"] .lead-fast-signout {
987
+ background: rgba(14, 165, 233, 0.2) !important;
988
+ }
989
+
990
+ .lead-fast-signout:hover {
991
+ background: rgba(0, 0, 0, 0.2) !important;
992
+ transform: translateY(-1px);
993
+ }
994
+
995
+ [data-theme="dark"] .lead-fast-signout:hover {
996
+ background: rgba(255, 255, 255, 0.2) !important;
997
+ }
998
+
999
+ [data-theme="cyberpunk"] .lead-fast-signout:hover {
1000
+ background: rgba(14, 165, 233, 0.3) !important;
1001
+ }
1002
+
1003
+ .lead-fast-branding {
1004
+ position: absolute;
1005
+ bottom: -20px;
1006
+ left: 24px;
1007
+ font-size: 9px;
1008
+ color: inherit !important;
1009
+ opacity: 0.4;
1010
+ font-weight: 500;
1011
+ letter-spacing: 0.05em;
1012
+ text-transform: lowercase;
1013
+ background: rgba(255, 255, 255, 0.8);
1014
+ padding: 2px 6px;
1015
+ border-radius: 4px;
1016
+ backdrop-filter: blur(8px);
1017
+ }
1018
+
1019
+ [data-theme="dark"] .lead-fast-branding {
1020
+ background: rgba(0, 0, 0, 0.6);
1021
+ }
1022
+
1023
+ [data-theme="cyberpunk"] .lead-fast-branding {
1024
+ background: rgba(0, 20, 36, 0.8);
1025
+ }
1026
+
1027
+ /* Modal Theming */
1028
+ .download-limiter-modal {
1029
+ display: none;
1030
+ position: fixed;
1031
+ top: 0;
1032
+ left: 0;
1033
+ width: 100%;
1034
+ height: 100%;
1035
+ background: rgba(0,0,0,0.5);
1036
+ z-index: 9999;
1037
+ align-items: center;
1038
+ justify-content: center;
1039
+ }
1040
+ .download-limiter-modal.show { display: flex; }
1041
+
1042
+ .download-limiter-content {
1043
+ background: #faf7f5 !important;
1044
+ color: #291334 !important;
1045
+ padding: 2rem;
1046
+ border-radius: 12px;
1047
+ max-width: 400px;
1048
+ text-align: center;
1049
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
1050
+ border: 1px solid #e7e2df !important;
1051
+ font-family: system-ui, -apple-system, sans-serif;
1052
+ }
1053
+
1054
+ [data-theme="dark"] .download-limiter-content {
1055
+ background: #1f2937 !important;
1056
+ color: #f9fafb !important;
1057
+ border-color: #374151 !important;
1058
+ }
1059
+
1060
+ [data-theme="cyberpunk"] .download-limiter-content {
1061
+ background: #001424 !important;
1062
+ color: #0ea5e9 !important;
1063
+ border-color: #075985 !important;
1064
+ }
1065
+
1066
+ [data-theme="valentine"] .download-limiter-content {
1067
+ background: rgba(233, 30, 122, 0.05) !important;
1068
+ color: #831843 !important;
1069
+ border-color: #e91e7a !important;
1070
+ }
1071
+
1072
+ [data-theme="bumblebee"] .download-limiter-content {
1073
+ background: rgba(255, 248, 220, 0.95) !important;
1074
+ color: #1c1917 !important;
1075
+ border-color: rgba(254, 215, 170, 0.8) !important;
1076
+ }
1077
+
1078
+ [data-theme="garden"] .download-limiter-content {
1079
+ background: rgba(240, 253, 244, 0.95) !important;
1080
+ color: #14532d !important;
1081
+ border-color: rgba(134, 239, 172, 0.8) !important;
1082
+ }
1083
+
1084
+ [data-theme="emerald"] .download-limiter-content {
1085
+ background: rgba(236, 253, 245, 0.95) !important;
1086
+ color: #065f46 !important;
1087
+ border-color: rgba(167, 243, 208, 0.8) !important;
1088
+ }
1089
+
1090
+ [data-theme="corporate"] .download-limiter-content {
1091
+ background: rgba(248, 250, 252, 0.95) !important;
1092
+ color: #1e293b !important;
1093
+ border-color: rgba(226, 232, 240, 0.8) !important;
1094
+ }
1095
+
1096
+ [data-theme="forest"] .download-limiter-content {
1097
+ background: rgba(23, 46, 23, 0.95) !important;
1098
+ color: #22c55e !important;
1099
+ border-color: rgba(34, 197, 94, 0.8) !important;
1100
+ }
1101
+
1102
+ [data-theme="halloween"] .download-limiter-content {
1103
+ background: rgba(26, 26, 26, 0.95) !important;
1104
+ color: #ff6500 !important;
1105
+ border-color: rgba(255, 165, 0, 0.3) !important;
1106
+ }
1107
+
1108
+ [data-theme="synthwave"] .download-limiter-content {
1109
+ background: rgba(32, 20, 64, 0.95) !important;
1110
+ color: #ba55d3 !important;
1111
+ border-color: rgba(186, 85, 211, 0.3) !important;
1112
+ }
1113
+
1114
+ [data-theme="dracula"] .download-limiter-content {
1115
+ background: rgba(40, 42, 54, 0.95) !important;
1116
+ color: #f8f8f2 !important;
1117
+ border-color: rgba(98, 114, 164, 0.3) !important;
1118
+ }
1119
+
1120
+ [data-theme="luxury"] .download-limiter-content {
1121
+ background: rgba(9, 9, 11, 0.95) !important;
1122
+ color: #d4af37 !important;
1123
+ border-color: rgba(212, 175, 55, 0.3) !important;
1124
+ }
1125
+
1126
+ [data-theme="night"] .download-limiter-content {
1127
+ background: rgba(15, 23, 42, 0.95) !important;
1128
+ color: #1e40af !important;
1129
+ border-color: rgba(30, 58, 138, 0.3) !important;
1130
+ }
1131
+
1132
+ [data-theme="light"] .download-limiter-content {
1133
+ background: rgba(255, 255, 255, 0.95) !important;
1134
+ color: #1f2937 !important;
1135
+ border-color: rgba(229, 231, 235, 0.8) !important;
1136
+ }
1137
+
1138
+ [data-theme="cupcake"] .download-limiter-content {
1139
+ background: rgba(250, 235, 215, 0.95) !important;
1140
+ color: #8b4513 !important;
1141
+ border-color: rgba(219, 185, 156, 0.8) !important;
1142
+ }
1143
+
1144
+ [data-theme="retro"] .download-limiter-content {
1145
+ background: rgba(212, 165, 116, 0.95) !important;
1146
+ color: #5d4037 !important;
1147
+ border-color: rgba(185, 144, 102, 0.8) !important;
1148
+ }
1149
+
1150
+ [data-theme="aqua"] .download-limiter-content {
1151
+ background: rgba(240, 253, 250, 0.95) !important;
1152
+ color: #155e75 !important;
1153
+ border-color: rgba(125, 211, 252, 0.8) !important;
1154
+ }
1155
+
1156
+ [data-theme="lofi"] .download-limiter-content {
1157
+ background: rgba(248, 248, 248, 0.95) !important;
1158
+ color: #1a1a1a !important;
1159
+ border-color: rgba(68, 68, 68, 0.8) !important;
1160
+ }
1161
+
1162
+ [data-theme="pastel"] .download-limiter-content {
1163
+ background: rgba(254, 251, 255, 0.95) !important;
1164
+ color: #7c3aed !important;
1165
+ border-color: rgba(209, 196, 233, 0.8) !important;
1166
+ }
1167
+
1168
+ [data-theme="fantasy"] .download-limiter-content {
1169
+ background: rgba(255, 247, 237, 0.95) !important;
1170
+ color: #be185d !important;
1171
+ border-color: rgba(249, 168, 212, 0.8) !important;
1172
+ }
1173
+
1174
+ [data-theme="wireframe"] .download-limiter-content {
1175
+ background: rgba(255, 255, 255, 0.95) !important;
1176
+ color: #000000 !important;
1177
+ border-color: rgba(0, 0, 0, 0.3) !important;
1178
+ }
1179
+
1180
+ [data-theme="black"] .download-limiter-content {
1181
+ background: rgba(0, 0, 0, 0.95) !important;
1182
+ color: #ffffff !important;
1183
+ border-color: rgba(55, 55, 55, 0.8) !important;
1184
+ }
1185
+
1186
+ [data-theme="cmyk"] .download-limiter-content {
1187
+ background: rgba(0, 255, 255, 0.95) !important;
1188
+ color: #000000 !important;
1189
+ border-color: rgba(255, 0, 255, 0.8) !important;
1190
+ }
1191
+
1192
+ [data-theme="autumn"] .download-limiter-content {
1193
+ background: rgba(139, 69, 19, 0.95) !important;
1194
+ color: #ff8c00 !important;
1195
+ border-color: rgba(255, 140, 0, 0.8) !important;
1196
+ }
1197
+
1198
+ [data-theme="business"] .download-limiter-content {
1199
+ background: rgba(29, 78, 216, 0.95) !important;
1200
+ color: #3b82f6 !important;
1201
+ border-color: rgba(59, 130, 246, 0.8) !important;
1202
+ }
1203
+
1204
+ [data-theme="acid"] .download-limiter-content {
1205
+ background: rgba(255, 255, 0, 0.95) !important;
1206
+ color: #000000 !important;
1207
+ border-color: rgba(255, 0, 255, 0.8) !important;
1208
+ }
1209
+
1210
+ [data-theme="lemonade"] .download-limiter-content {
1211
+ background: rgba(255, 255, 224, 0.95) !important;
1212
+ color: #15803d !important;
1213
+ border-color: rgba(34, 197, 94, 0.8) !important;
1214
+ }
1215
+
1216
+ [data-theme="coffee"] .download-limiter-content {
1217
+ background: rgba(101, 67, 33, 0.95) !important;
1218
+ color: #d2b48c !important;
1219
+ border-color: rgba(160, 82, 45, 0.8) !important;
1220
+ }
1221
+
1222
+ [data-theme="winter"] .download-limiter-content {
1223
+ background: rgba(248, 250, 252, 0.95) !important;
1224
+ color: #1e40af !important;
1225
+ border-color: rgba(59, 130, 246, 0.8) !important;
1226
+ }
1227
+
1228
+ .download-limiter-content h2 {
1229
+ color: inherit !important;
1230
+ margin: 0 0 1rem 0;
1231
+ font-size: 1.5rem;
1232
+ font-weight: 600;
1233
+ }
1234
+ .download-limiter-content p {
1235
+ color: inherit !important;
1236
+ margin: 0 0 1.5rem 0;
1237
+ line-height: 1.6;
1238
+ }
1239
+ .download-limiter-signin-btn {
1240
+ display: inline-flex;
1241
+ align-items: center;
1242
+ gap: 8px;
1243
+ background: #65c3c8 !important;
1244
+ color: #291334 !important;
1245
+ border: none;
1246
+ padding: 12px 20px;
1247
+ border-radius: 8px;
1248
+ font-size: 14px;
1249
+ font-weight: 500;
1250
+ cursor: pointer;
1251
+ text-decoration: none;
1252
+ transition: all 0.2s;
1253
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1254
+ }
1255
+
1256
+ [data-theme="dark"] .download-limiter-signin-btn {
1257
+ background: #3b82f6 !important;
1258
+ color: #ffffff !important;
1259
+ }
1260
+
1261
+ [data-theme="cyberpunk"] .download-limiter-signin-btn {
1262
+ background: #f59e0b !important;
1263
+ color: #000000 !important;
1264
+ }
1265
+
1266
+ [data-theme="valentine"] .download-limiter-signin-btn {
1267
+ background: #e91e7a !important;
1268
+ color: #ffffff !important;
1269
+ }
1270
+
1271
+ [data-theme="bumblebee"] .download-limiter-signin-btn {
1272
+ background: #fbbf24 !important;
1273
+ color: #1c1917 !important;
1274
+ }
1275
+
1276
+ [data-theme="garden"] .download-limiter-signin-btn {
1277
+ background: #22c55e !important;
1278
+ color: #ffffff !important;
1279
+ }
1280
+
1281
+ [data-theme="emerald"] .download-limiter-signin-btn {
1282
+ background: #10b981 !important;
1283
+ color: #ffffff !important;
1284
+ }
1285
+
1286
+ [data-theme="corporate"] .download-limiter-signin-btn {
1287
+ background: #3b82f6 !important;
1288
+ color: #ffffff !important;
1289
+ }
1290
+
1291
+ [data-theme="forest"] .download-limiter-signin-btn {
1292
+ background: #22c55e !important;
1293
+ color: #1a1a1a !important;
1294
+ }
1295
+
1296
+ [data-theme="halloween"] .download-limiter-signin-btn {
1297
+ background: #ff6500 !important;
1298
+ color: #1a1a1a !important;
1299
+ }
1300
+
1301
+ [data-theme="synthwave"] .download-limiter-signin-btn {
1302
+ background: #ba55d3 !important;
1303
+ color: #201040 !important;
1304
+ }
1305
+
1306
+ [data-theme="dracula"] .download-limiter-signin-btn {
1307
+ background: #8be9fd !important;
1308
+ color: #282a36 !important;
1309
+ }
1310
+
1311
+ [data-theme="luxury"] .download-limiter-signin-btn {
1312
+ background: #d4af37 !important;
1313
+ color: #09090b !important;
1314
+ }
1315
+
1316
+ [data-theme="night"] .download-limiter-signin-btn {
1317
+ background: #1e40af !important;
1318
+ color: #ffffff !important;
1319
+ }
1320
+
1321
+ [data-theme="light"] .download-limiter-signin-btn {
1322
+ background: #3b82f6 !important;
1323
+ color: #ffffff !important;
1324
+ }
1325
+
1326
+ [data-theme="cupcake"] .download-limiter-signin-btn {
1327
+ background: #8b4513 !important;
1328
+ color: #ffffff !important;
1329
+ }
1330
+
1331
+ [data-theme="retro"] .download-limiter-signin-btn {
1332
+ background: #d4a574 !important;
1333
+ color: #ffffff !important;
1334
+ }
1335
+
1336
+ [data-theme="aqua"] .download-limiter-signin-btn {
1337
+ background: #0891b2 !important;
1338
+ color: #ffffff !important;
1339
+ }
1340
+
1341
+ [data-theme="lofi"] .download-limiter-signin-btn {
1342
+ background: #1a1a1a !important;
1343
+ color: #ffffff !important;
1344
+ }
1345
+
1346
+ [data-theme="pastel"] .download-limiter-signin-btn {
1347
+ background: #7c3aed !important;
1348
+ color: #ffffff !important;
1349
+ }
1350
+
1351
+ [data-theme="fantasy"] .download-limiter-signin-btn {
1352
+ background: #be185d !important;
1353
+ color: #ffffff !important;
1354
+ }
1355
+
1356
+ [data-theme="wireframe"] .download-limiter-signin-btn {
1357
+ background: #dfdfdf !important;
1358
+ color: #000000 !important;
1359
+ border: 1px solid #000000 !important;
1360
+ }
1361
+
1362
+ [data-theme="black"] .download-limiter-signin-btn {
1363
+ background: #ffffff !important;
1364
+ color: #000000 !important;
1365
+ }
1366
+
1367
+ [data-theme="cmyk"] .download-limiter-signin-btn {
1368
+ background: #ff00ff !important;
1369
+ color: #000000 !important;
1370
+ }
1371
+
1372
+ [data-theme="autumn"] .download-limiter-signin-btn {
1373
+ background: #ff8c00 !important;
1374
+ color: #ffffff !important;
1375
+ }
1376
+
1377
+ [data-theme="business"] .download-limiter-signin-btn {
1378
+ background: #3b82f6 !important;
1379
+ color: #ffffff !important;
1380
+ }
1381
+
1382
+ [data-theme="acid"] .download-limiter-signin-btn {
1383
+ background: #ff00ff !important;
1384
+ color: #000000 !important;
1385
+ }
1386
+
1387
+ [data-theme="lemonade"] .download-limiter-signin-btn {
1388
+ background: #22c55e !important;
1389
+ color: #ffffff !important;
1390
+ }
1391
+
1392
+ [data-theme="coffee"] .download-limiter-signin-btn {
1393
+ background: #a0522d !important;
1394
+ color: #ffffff !important;
1395
+ }
1396
+
1397
+ [data-theme="winter"] .download-limiter-signin-btn {
1398
+ background: #3b82f6 !important;
1399
+ color: #ffffff !important;
1400
+ }
1401
+
1402
+ .download-limiter-signin-btn:hover {
1403
+ opacity: 0.9;
1404
+ transform: translateY(-1px);
1405
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
1406
+ }
1407
+
1408
+ /* Upgrade Button Styles for All Themes */
1409
+ [data-theme="dark"] .upgrade-btn {
1410
+ background: #3b82f6 !important;
1411
+ color: #ffffff !important;
1412
+ }
1413
+
1414
+ [data-theme="cyberpunk"] .upgrade-btn {
1415
+ background: #ff073a !important;
1416
+ color: #ffffff !important;
1417
+ }
1418
+
1419
+ [data-theme="valentine"] .upgrade-btn {
1420
+ background: #e91e7a !important;
1421
+ color: #ffffff !important;
1422
+ }
1423
+
1424
+ [data-theme="bumblebee"] .upgrade-btn {
1425
+ background: #f59e0b !important;
1426
+ color: #ffffff !important;
1427
+ }
1428
+
1429
+ [data-theme="garden"] .upgrade-btn {
1430
+ background: #10b981 !important;
1431
+ color: #ffffff !important;
1432
+ }
1433
+
1434
+ [data-theme="emerald"] .upgrade-btn {
1435
+ background: #10b981 !important;
1436
+ color: #ffffff !important;
1437
+ }
1438
+
1439
+ [data-theme="corporate"] .upgrade-btn {
1440
+ background: #3b82f6 !important;
1441
+ color: #ffffff !important;
1442
+ }
1443
+
1444
+ [data-theme="forest"] .upgrade-btn {
1445
+ background: #10b981 !important;
1446
+ color: #ffffff !important;
1447
+ }
1448
+
1449
+ [data-theme="halloween"] .upgrade-btn {
1450
+ background: #ff7b00 !important;
1451
+ color: #000000 !important;
1452
+ }
1453
+
1454
+ [data-theme="synthwave"] .upgrade-btn {
1455
+ background: #e879f9 !important;
1456
+ color: #ffffff !important;
1457
+ }
1458
+
1459
+ [data-theme="dracula"] .upgrade-btn {
1460
+ background: #bd93f9 !important;
1461
+ color: #ffffff !important;
1462
+ }
1463
+
1464
+ [data-theme="luxury"] .upgrade-btn {
1465
+ background: #d4af37 !important;
1466
+ color: #000000 !important;
1467
+ }
1468
+
1469
+ [data-theme="night"] .upgrade-btn {
1470
+ background: #38bdf8 !important;
1471
+ color: #ffffff !important;
1472
+ }
1473
+
1474
+ [data-theme="light"] .upgrade-btn {
1475
+ background: #3b82f6 !important;
1476
+ color: #ffffff !important;
1477
+ }
1478
+
1479
+ [data-theme="cupcake"] .upgrade-btn {
1480
+ background: #f472b6 !important;
1481
+ color: #ffffff !important;
1482
+ }
1483
+
1484
+ [data-theme="retro"] .upgrade-btn {
1485
+ background: #d4a574 !important;
1486
+ color: #ffffff !important;
1487
+ }
1488
+
1489
+ [data-theme="aqua"] .upgrade-btn {
1490
+ background: #06b6d4 !important;
1491
+ color: #ffffff !important;
1492
+ }
1493
+
1494
+ [data-theme="lofi"] .upgrade-btn {
1495
+ background: #a3a3a3 !important;
1496
+ color: #000000 !important;
1497
+ }
1498
+
1499
+ [data-theme="pastel"] .upgrade-btn {
1500
+ background: #a78bfa !important;
1501
+ color: #ffffff !important;
1502
+ }
1503
+
1504
+ [data-theme="fantasy"] .upgrade-btn {
1505
+ background: #f472b6 !important;
1506
+ color: #ffffff !important;
1507
+ }
1508
+
1509
+ [data-theme="wireframe"] .upgrade-btn {
1510
+ background: #dfdfdf !important;
1511
+ color: #000000 !important;
1512
+ border: 1px solid #000000 !important;
1513
+ }
1514
+
1515
+ [data-theme="black"] .upgrade-btn {
1516
+ background: #ffffff !important;
1517
+ color: #000000 !important;
1518
+ }
1519
+
1520
+ [data-theme="cmyk"] .upgrade-btn {
1521
+ background: #0891b2 !important;
1522
+ color: #ffffff !important;
1523
+ }
1524
+
1525
+ [data-theme="autumn"] .upgrade-btn {
1526
+ background: #d97706 !important;
1527
+ color: #ffffff !important;
1528
+ }
1529
+
1530
+ [data-theme="business"] .upgrade-btn {
1531
+ background: #3b82f6 !important;
1532
+ color: #ffffff !important;
1533
+ }
1534
+
1535
+ [data-theme="acid"] .upgrade-btn {
1536
+ background: #84cc16 !important;
1537
+ color: #000000 !important;
1538
+ }
1539
+
1540
+ [data-theme="lemonade"] .upgrade-btn {
1541
+ background: #22c55e !important;
1542
+ color: #ffffff !important;
1543
+ }
1544
+
1545
+ [data-theme="coffee"] .upgrade-btn {
1546
+ background: #a0522d !important;
1547
+ color: #ffffff !important;
1548
+ }
1549
+
1550
+ [data-theme="winter"] .upgrade-btn {
1551
+ background: #3b82f6 !important;
1552
+ color: #ffffff !important;
1553
+ }
1554
+
1555
+ /* Cancel Button Styles for All Themes */
1556
+ [data-theme="dark"] .cancel-btn {
1557
+ background: #4b5563 !important;
1558
+ color: #ffffff !important;
1559
+ }
1560
+
1561
+ [data-theme="cyberpunk"] .cancel-btn {
1562
+ background: #7e22ce !important;
1563
+ color: #ffffff !important;
1564
+ }
1565
+
1566
+ [data-theme="valentine"] .cancel-btn {
1567
+ background: #c11560 !important;
1568
+ color: #ffffff !important;
1569
+ }
1570
+
1571
+ [data-theme="bumblebee"] .cancel-btn {
1572
+ background: #d97706 !important;
1573
+ color: #ffffff !important;
1574
+ }
1575
+
1576
+ [data-theme="garden"] .cancel-btn {
1577
+ background: #059669 !important;
1578
+ color: #ffffff !important;
1579
+ }
1580
+
1581
+ [data-theme="emerald"] .cancel-btn {
1582
+ background: #059669 !important;
1583
+ color: #ffffff !important;
1584
+ }
1585
+
1586
+ [data-theme="corporate"] .cancel-btn {
1587
+ background: #6b7280 !important;
1588
+ color: #ffffff !important;
1589
+ }
1590
+
1591
+ [data-theme="forest"] .cancel-btn {
1592
+ background: #059669 !important;
1593
+ color: #ffffff !important;
1594
+ }
1595
+
1596
+ [data-theme="halloween"] .cancel-btn {
1597
+ background: #7c3aed !important;
1598
+ color: #ffffff !important;
1599
+ }
1600
+
1601
+ [data-theme="synthwave"] .cancel-btn {
1602
+ background: #7e22ce !important;
1603
+ color: #ffffff !important;
1604
+ }
1605
+
1606
+ [data-theme="dracula"] .cancel-btn {
1607
+ background: #6272a4 !important;
1608
+ color: #ffffff !important;
1609
+ }
1610
+
1611
+ [data-theme="luxury"] .cancel-btn {
1612
+ background: #8b5cf6 !important;
1613
+ color: #ffffff !important;
1614
+ }
1615
+
1616
+ [data-theme="night"] .cancel-btn {
1617
+ background: #0284c7 !important;
1618
+ color: #ffffff !important;
1619
+ }
1620
+
1621
+ [data-theme="light"] .cancel-btn {
1622
+ background: #6b7280 !important;
1623
+ color: #ffffff !important;
1624
+ }
1625
+
1626
+ [data-theme="cupcake"] .cancel-btn {
1627
+ background: #ec4899 !important;
1628
+ color: #ffffff !important;
1629
+ }
1630
+
1631
+ [data-theme="retro"] .cancel-btn {
1632
+ background: #b8925c !important;
1633
+ color: #ffffff !important;
1634
+ }
1635
+
1636
+ [data-theme="aqua"] .cancel-btn {
1637
+ background: #0891b2 !important;
1638
+ color: #ffffff !important;
1639
+ }
1640
+
1641
+ [data-theme="lofi"] .cancel-btn {
1642
+ background: #6b7280 !important;
1643
+ color: #ffffff !important;
1644
+ }
1645
+
1646
+ [data-theme="pastel"] .cancel-btn {
1647
+ background: #8b5cf6 !important;
1648
+ color: #ffffff !important;
1649
+ }
1650
+
1651
+ [data-theme="fantasy"] .cancel-btn {
1652
+ background: #ec4899 !important;
1653
+ color: #ffffff !important;
1654
+ }
1655
+
1656
+ [data-theme="wireframe"] .cancel-btn {
1657
+ background: #f5f5f5 !important;
1658
+ color: #000000 !important;
1659
+ border: 1px solid #000000 !important;
1660
+ }
1661
+
1662
+ [data-theme="black"] .cancel-btn {
1663
+ background: #6b7280 !important;
1664
+ color: #ffffff !important;
1665
+ }
1666
+
1667
+ [data-theme="cmyk"] .cancel-btn {
1668
+ background: #0369a1 !important;
1669
+ color: #ffffff !important;
1670
+ }
1671
+
1672
+ [data-theme="autumn"] .cancel-btn {
1673
+ background: #c2410c !important;
1674
+ color: #ffffff !important;
1675
+ }
1676
+
1677
+ [data-theme="business"] .cancel-btn {
1678
+ background: #6b7280 !important;
1679
+ color: #ffffff !important;
1680
+ }
1681
+
1682
+ [data-theme="acid"] .cancel-btn {
1683
+ background: #65a30d !important;
1684
+ color: #ffffff !important;
1685
+ }
1686
+
1687
+ [data-theme="lemonade"] .cancel-btn {
1688
+ background: #16a34a !important;
1689
+ color: #ffffff !important;
1690
+ }
1691
+
1692
+ [data-theme="coffee"] .cancel-btn {
1693
+ background: #8b4513 !important;
1694
+ color: #ffffff !important;
1695
+ }
1696
+
1697
+ [data-theme="winter"] .cancel-btn {
1698
+ background: #1e40af !important;
1699
+ color: #ffffff !important;
1700
+ }
1701
+
1702
+ /* Success Button Styles for All Themes */
1703
+ [data-theme="dark"] .success-btn {
1704
+ background: #10b981 !important;
1705
+ color: #ffffff !important;
1706
+ }
1707
+
1708
+ [data-theme="cyberpunk"] .success-btn {
1709
+ background: #00ff41 !important;
1710
+ color: #000000 !important;
1711
+ }
1712
+
1713
+ [data-theme="valentine"] .success-btn {
1714
+ background: #e91e7a !important;
1715
+ color: #ffffff !important;
1716
+ }
1717
+
1718
+ [data-theme="bumblebee"] .success-btn {
1719
+ background: #f59e0b !important;
1720
+ color: #ffffff !important;
1721
+ }
1722
+
1723
+ [data-theme="garden"] .success-btn {
1724
+ background: #10b981 !important;
1725
+ color: #ffffff !important;
1726
+ }
1727
+
1728
+ [data-theme="emerald"] .success-btn {
1729
+ background: #10b981 !important;
1730
+ color: #ffffff !important;
1731
+ }
1732
+
1733
+ [data-theme="corporate"] .success-btn {
1734
+ background: #10b981 !important;
1735
+ color: #ffffff !important;
1736
+ }
1737
+
1738
+ [data-theme="forest"] .success-btn {
1739
+ background: #10b981 !important;
1740
+ color: #ffffff !important;
1741
+ }
1742
+
1743
+ [data-theme="halloween"] .success-btn {
1744
+ background: #ff7b00 !important;
1745
+ color: #000000 !important;
1746
+ }
1747
+
1748
+ [data-theme="synthwave"] .success-btn {
1749
+ background: #00ff41 !important;
1750
+ color: #000000 !important;
1751
+ }
1752
+
1753
+ [data-theme="dracula"] .success-btn {
1754
+ background: #50fa7b !important;
1755
+ color: #000000 !important;
1756
+ }
1757
+
1758
+ [data-theme="luxury"] .success-btn {
1759
+ background: #d4af37 !important;
1760
+ color: #000000 !important;
1761
+ }
1762
+
1763
+ [data-theme="night"] .success-btn {
1764
+ background: #22d3ee !important;
1765
+ color: #ffffff !important;
1766
+ }
1767
+
1768
+ [data-theme="light"] .success-btn {
1769
+ background: #10b981 !important;
1770
+ color: #ffffff !important;
1771
+ }
1772
+
1773
+ [data-theme="cupcake"] .success-btn {
1774
+ background: #f472b6 !important;
1775
+ color: #ffffff !important;
1776
+ }
1777
+
1778
+ [data-theme="retro"] .success-btn {
1779
+ background: #d4a574 !important;
1780
+ color: #ffffff !important;
1781
+ }
1782
+
1783
+ [data-theme="aqua"] .success-btn {
1784
+ background: #06b6d4 !important;
1785
+ color: #ffffff !important;
1786
+ }
1787
+
1788
+ [data-theme="lofi"] .success-btn {
1789
+ background: #10b981 !important;
1790
+ color: #ffffff !important;
1791
+ }
1792
+
1793
+ [data-theme="pastel"] .success-btn {
1794
+ background: #34d399 !important;
1795
+ color: #ffffff !important;
1796
+ }
1797
+
1798
+ [data-theme="fantasy"] .success-btn {
1799
+ background: #f472b6 !important;
1800
+ color: #ffffff !important;
1801
+ }
1802
+
1803
+ [data-theme="wireframe"] .success-btn {
1804
+ background: #dfdfdf !important;
1805
+ color: #000000 !important;
1806
+ border: 1px solid #000000 !important;
1807
+ }
1808
+
1809
+ [data-theme="black"] .success-btn {
1810
+ background: #10b981 !important;
1811
+ color: #ffffff !important;
1812
+ }
1813
+
1814
+ [data-theme="cmyk"] .success-btn {
1815
+ background: #06b6d4 !important;
1816
+ color: #ffffff !important;
1817
+ }
1818
+
1819
+ [data-theme="autumn"] .success-btn {
1820
+ background: #d97706 !important;
1821
+ color: #ffffff !important;
1822
+ }
1823
+
1824
+ [data-theme="business"] .success-btn {
1825
+ background: #10b981 !important;
1826
+ color: #ffffff !important;
1827
+ }
1828
+
1829
+ [data-theme="acid"] .success-btn {
1830
+ background: #84cc16 !important;
1831
+ color: #000000 !important;
1832
+ }
1833
+
1834
+ [data-theme="lemonade"] .success-btn {
1835
+ background: #22c55e !important;
1836
+ color: #ffffff !important;
1837
+ }
1838
+
1839
+ [data-theme="coffee"] .success-btn {
1840
+ background: #a0522d !important;
1841
+ color: #ffffff !important;
1842
+ }
1843
+
1844
+ [data-theme="winter"] .success-btn {
1845
+ background: #3b82f6 !important;
1846
+ color: #ffffff !important;
1847
+ }
1848
+ .download-limiter-signout-btn {
1849
+ background: #dc3545;
1850
+ color: white;
1851
+ border: none;
1852
+ padding: 6px 12px;
1853
+ border-radius: 4px;
1854
+ font-size: 12px;
1855
+ cursor: pointer;
1856
+ margin-left: 8px;
1857
+ }
1858
+ .download-limiter-premium-badge {
1859
+ background: linear-gradient(45deg, #f39c12, #e74c3c);
1860
+ color: white;
1861
+ padding: 2px 6px;
1862
+ border-radius: 3px;
1863
+ font-size: 10px;
1864
+ margin-left: 8px;
1865
+ font-weight: bold;
1866
+ }
1867
+ .download-limiter-btn-premium {
1868
+ opacity: 0.7;
1869
+ }
1870
+ .download-limiter-success-modal {
1871
+ display: none;
1872
+ position: fixed;
1873
+ top: 0;
1874
+ left: 0;
1875
+ width: 100%;
1876
+ height: 100%;
1877
+ background: rgba(0,0,0,0.5);
1878
+ z-index: 10000;
1879
+ align-items: center;
1880
+ justify-content: center;
1881
+ }
1882
+ .download-limiter-success-modal.show { display: flex; }
1883
+ .download-limiter-success-content {
1884
+ background: white;
1885
+ padding: 2rem;
1886
+ border-radius: 8px;
1887
+ max-width: 400px;
1888
+ text-align: center;
1889
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
1890
+ border-left: 4px solid #38a169;
1891
+ }
1892
+
1893
+ /* Single upgrade flow styles */
1894
+ .single-upgrade-container {
1895
+ margin: 20px 0;
1896
+ text-align: center;
1897
+ }
1898
+
1899
+ .single-upgrade-content .upgrade-btn {
1900
+ background: linear-gradient(135deg, #059669, #0d9488) !important;
1901
+ color: white !important;
1902
+ border: none !important;
1903
+ padding: 16px 32px !important;
1904
+ border-radius: 12px !important;
1905
+ font-size: 18px !important;
1906
+ font-weight: 600 !important;
1907
+ cursor: pointer !important;
1908
+ transition: all 0.3s ease !important;
1909
+ box-shadow: 0 4px 12px rgba(5, 150, 105, 0.3) !important;
1910
+ margin-bottom: 16px !important;
1911
+ min-width: 200px !important;
1912
+ }
1913
+
1914
+ .single-upgrade-content .upgrade-btn:hover {
1915
+ background: linear-gradient(135deg, #047857, #0f766e) !important;
1916
+ transform: translateY(-2px) !important;
1917
+ box-shadow: 0 6px 20px rgba(5, 150, 105, 0.4) !important;
1918
+ }
1919
+
1920
+ .single-upgrade-content .upgrade-description {
1921
+ font-size: 14px !important;
1922
+ color: #6b7280 !important;
1923
+ margin: 12px 0 0 0 !important;
1924
+ line-height: 1.5 !important;
1925
+ max-width: 300px !important;
1926
+ margin-left: auto !important;
1927
+ margin-right: auto !important;
1928
+ }
1929
+
1930
+ /* Anonymous user usage count styles */
1931
+ .lead-fast-anonymous-status {
1932
+ display: flex;
1933
+ align-items: center;
1934
+ gap: 12px;
1935
+ }
1936
+
1937
+ .lead-fast-anonymous-status .lead-fast-count {
1938
+ background: rgba(59, 130, 246, 0.1);
1939
+ color: #1d4ed8;
1940
+ padding: 6px 12px;
1941
+ border-radius: 8px;
1942
+ font-size: 14px;
1943
+ font-weight: 500;
1944
+ border: 1px solid rgba(59, 130, 246, 0.2);
1945
+ }
1946
+
1947
+ /* Success button theming */
1948
+ .download-limiter-btn.success-btn {
1949
+ background: #059669 !important;
1950
+ color: white !important;
1951
+ border: none !important;
1952
+ padding: 12px 24px !important;
1953
+ border-radius: 8px !important;
1954
+ cursor: pointer !important;
1955
+ margin-top: 20px !important;
1956
+ font-size: 16px !important;
1957
+ font-weight: 500 !important;
1958
+ transition: all 0.2s ease !important;
1959
+ }
1960
+
1961
+ .download-limiter-btn.success-btn:hover {
1962
+ background: #047857 !important;
1963
+ transform: translateY(-1px) !important;
1964
+ }
1965
+
1966
+ /* Theme-specific success button colors */
1967
+ [data-theme="cyberpunk"] .download-limiter-btn.success-btn {
1968
+ background: #0ea5e9 !important;
1969
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3) !important;
1970
+ }
1971
+
1972
+ [data-theme="cyberpunk"] .download-limiter-btn.success-btn:hover {
1973
+ background: #0284c7 !important;
1974
+ }
1975
+
1976
+ [data-theme="valentine"] .download-limiter-btn.success-btn {
1977
+ background: #be185d !important;
1978
+ }
1979
+
1980
+ [data-theme="valentine"] .download-limiter-btn.success-btn:hover {
1981
+ background: #9d174d !important;
1982
+ }
1983
+
1984
+ [data-theme="bumblebee"] .download-limiter-btn.success-btn {
1985
+ background: #f59e0b !important;
1986
+ }
1987
+
1988
+ [data-theme="bumblebee"] .download-limiter-btn.success-btn:hover {
1989
+ background: #d97706 !important;
1990
+ }
1991
+
1992
+ [data-theme="garden"] .download-limiter-btn.success-btn {
1993
+ background: #16a34a !important;
1994
+ }
1995
+
1996
+ [data-theme="garden"] .download-limiter-btn.success-btn:hover {
1997
+ background: #15803d !important;
1998
+ }
1999
+
2000
+ /* Google Sign-in button theming */
2001
+ .download-limiter-btn.google-signin-btn {
2002
+ background: #4285f4 !important;
2003
+ color: white !important;
2004
+ border: none !important;
2005
+ padding: 12px 24px !important;
2006
+ border-radius: 8px !important;
2007
+ cursor: pointer !important;
2008
+ margin-right: 10px !important;
2009
+ display: inline-flex !important;
2010
+ align-items: center !important;
2011
+ gap: 8px !important;
2012
+ font-size: 14px !important;
2013
+ font-weight: 500 !important;
2014
+ transition: all 0.2s ease !important;
2015
+ }
2016
+
2017
+ .download-limiter-btn.google-signin-btn:hover {
2018
+ background: #3367d6 !important;
2019
+ transform: translateY(-1px) !important;
2020
+ }
2021
+
2022
+ /* Upgrade button theming */
2023
+ .download-limiter-btn.upgrade-btn {
2024
+ background: #059669 !important;
2025
+ color: white !important;
2026
+ border: none !important;
2027
+ padding: 12px 24px !important;
2028
+ border-radius: 8px !important;
2029
+ cursor: pointer !important;
2030
+ margin-right: 10px !important;
2031
+ font-size: 16px !important;
2032
+ font-weight: 500 !important;
2033
+ transition: all 0.2s ease !important;
2034
+ }
2035
+
2036
+ .download-limiter-btn.upgrade-btn:hover {
2037
+ background: #047857 !important;
2038
+ transform: translateY(-1px) !important;
2039
+ }
2040
+
2041
+ /* Theme-specific button colors */
2042
+ [data-theme="cyberpunk"] .download-limiter-btn.upgrade-btn {
2043
+ background: #0ea5e9 !important;
2044
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3) !important;
2045
+ }
2046
+
2047
+ [data-theme="valentine"] .download-limiter-btn.upgrade-btn {
2048
+ background: #be185d !important;
2049
+ }
2050
+
2051
+ [data-theme="bumblebee"] .download-limiter-btn.upgrade-btn {
2052
+ background: #f59e0b !important;
2053
+ }
2054
+
2055
+ [data-theme="garden"] .download-limiter-btn.upgrade-btn {
2056
+ background: #16a34a !important;
2057
+ }
2058
+
2059
+ /* Cancel button container and styling - positioned at very bottom */
2060
+ .download-limiter-cancel-container {
2061
+ margin-top: 30px !important;
2062
+ padding-top: 20px !important;
2063
+ border-top: 1px solid rgba(0, 0, 0, 0.1) !important;
2064
+ text-align: center !important;
2065
+ position: relative !important;
2066
+ bottom: 0 !important;
2067
+ }
2068
+
2069
+ .download-limiter-btn.cancel-btn {
2070
+ background: transparent !important;
2071
+ color: #6b7280 !important;
2072
+ border: none !important;
2073
+ padding: 8px 16px !important;
2074
+ border-radius: 6px !important;
2075
+ cursor: pointer !important;
2076
+ font-size: 13px !important;
2077
+ font-weight: 400 !important;
2078
+ transition: all 0.2s ease !important;
2079
+ text-decoration: underline !important;
2080
+ margin: 0 auto !important;
2081
+ display: block !important;
2082
+ }
2083
+
2084
+ .download-limiter-btn.cancel-btn:hover {
2085
+ color: #374151 !important;
2086
+ background: rgba(107, 114, 128, 0.1) !important;
2087
+ text-decoration: none !important;
2088
+ }
2089
+
2090
+ /* Theme-specific cancel button border */
2091
+ [data-theme="dark"] .download-limiter-cancel-container {
2092
+ border-top-color: rgba(255, 255, 255, 0.1) !important;
2093
+ }
2094
+
2095
+ [data-theme="dark"] .download-limiter-btn.cancel-btn {
2096
+ color: #9ca3af !important;
2097
+ }
2098
+
2099
+ [data-theme="dark"] .download-limiter-btn.cancel-btn:hover {
2100
+ color: #d1d5db !important;
2101
+ background: rgba(156, 163, 175, 0.1) !important;
2102
+ }
2103
+
2104
+ [data-theme="cyberpunk"] .download-limiter-cancel-container {
2105
+ border-top-color: rgba(14, 165, 233, 0.2) !important;
2106
+ }
2107
+
2108
+ [data-theme="cyberpunk"] .download-limiter-btn.cancel-btn {
2109
+ color: #0ea5e9 !important;
2110
+ }
2111
+
2112
+ [data-theme="cyberpunk"] .download-limiter-btn.cancel-btn:hover {
2113
+ color: #38bdf8 !important;
2114
+ background: rgba(14, 165, 233, 0.1) !important;
2115
+ }
2116
+ `;
2117
+ document.head.appendChild(styles);
2118
+
2119
+ }
2120
+
2121
+ private injectDaisyUI(): void {
2122
+ //console.log('🎨 [THEME DEBUG] injectDaisyUI called');
2123
+ //console.log('🎨 [THEME DEBUG] Theme config:', this.config.theme);
2124
+
2125
+ // Check if DaisyUI is already loaded
2126
+ const existingLink = document.querySelector('link[href*="daisyui"]');
2127
+ if (existingLink) {
2128
+ //console.log('🎨 [THEME DEBUG] DaisyUI already exists:', existingLink);
2129
+ // Apply theme if DaisyUI already exists
2130
+ if (this.config.theme?.name) {
2131
+ const themeName = this.config.theme.name.toLowerCase();
2132
+ //console.log('🎨 [THEME DEBUG] Setting existing theme:', themeName);
2133
+ document.documentElement.setAttribute('data-theme', themeName);
2134
+ //console.log('🎨 [THEME DEBUG] Current data-theme attribute:', document.documentElement.getAttribute('data-theme'));
2135
+ }
2136
+ return;
2137
+ }
2138
+
2139
+ //console.log('🎨 [THEME DEBUG] No existing DaisyUI found, injecting...');
2140
+
2141
+ // Note: Tailwind CSS is not needed as we use custom CSS for all styling
2142
+
2143
+ // Apply theme directly using our custom CSS - no external dependencies needed
2144
+ if (this.config.theme?.name) {
2145
+ const themeName = this.config.theme.name.toLowerCase();
2146
+ document.documentElement.setAttribute('data-theme', themeName);
2147
+
2148
+ // Update status displays immediately since no CSS loading is needed
2149
+ this.updateStatusDisplays();
2150
+ }
2151
+ }
2152
+
2153
+ private createPaywallModal(): void {
2154
+ if (document.getElementById('download-limiter-paywall')) return;
2155
+
2156
+ const modal = document.createElement('div');
2157
+ modal.id = 'download-limiter-paywall';
2158
+ modal.className = 'download-limiter-modal';
2159
+ modal.innerHTML = `
2160
+ <div class="download-limiter-content">
2161
+ <h3 id="download-limiter-paywall-title">Usage Limit Reached</h3>
2162
+ <p id="download-limiter-paywall-subtitle">You've used all your free attempts. Upgrade to premium for unlimited usage!</p>
2163
+
2164
+ <!-- Auth Section (shows when not signed in) -->
2165
+ <div id="download-limiter-auth-section" style="margin: 20px 0;">
2166
+ <p>Sign in to continue or upgrade to premium</p>
2167
+ <button id="download-limiter-signin-btn" class="download-limiter-btn google-signin-btn">
2168
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
2169
+ <path d="M16.51 8H8.98v3h4.3c-.18 1-.74 1.48-1.6 2.04v2.01h2.6a8.8 8.8 0 0 0 2.38-5.88c0-.57-.05-.66-.15-1.18z" fill="white"/>
2170
+ <path d="M8.98 17c2.16 0 3.97-.72 5.3-1.94l-2.6-2.04a4.8 4.8 0 0 1-2.7.75 4.8 4.8 0 0 1-4.52-3.4H1.83v2.07A8 8 0 0 0 8.98 17z" fill="white"/>
2171
+ <path d="M4.46 10.37a4.8 4.8 0 0 1-.25-1.37c0-.48.09-.94.25-1.37V5.56H1.83a8 8 0 0 0 0 6.88l2.63-2.07z" fill="white"/>
2172
+ <path d="M8.98 3.77c1.32 0 2.5.45 3.44 1.35l2.58-2.58C13.94.64 11.66 0 8.98 0A8 8 0 0 0 1.83 5.56l2.63 2.07c.61-1.8 2.26-3.86 4.52-3.86z" fill="white"/>
2173
+ </svg>
2174
+ Sign in with Google
2175
+ </button>
2176
+ </div>
2177
+
2178
+ <!-- Payment Section (shows when signed in) -->
2179
+ <div id="download-limiter-payment-section" style="margin: 20px 0; display: none;">
2180
+ <button id="download-limiter-upgrade-btn" class="download-limiter-btn upgrade-btn">
2181
+ Upgrade to Premium
2182
+ </button>
2183
+ </div>
2184
+
2185
+ <div class="download-limiter-cancel-container">
2186
+ <button id="download-limiter-close-btn" class="download-limiter-btn cancel-btn">
2187
+ Cancel
2188
+ </button>
2189
+ </div>
2190
+ </div>
2191
+ `;
2192
+ document.body.appendChild(modal);
2193
+
2194
+ // Set up event handlers
2195
+ modal.querySelector('#download-limiter-signin-btn')!.addEventListener('click', async () => {
2196
+ await this.signIn();
2197
+ });
2198
+
2199
+ modal.querySelector('#download-limiter-upgrade-btn')!.addEventListener('click', async () => {
2200
+ const payment = await this.createPayment();
2201
+ if (payment?.checkout_url) {
2202
+ window.location.href = payment.checkout_url;
2203
+ }
2204
+ });
2205
+
2206
+ modal.querySelector('#download-limiter-close-btn')!.addEventListener('click', () => {
2207
+ if (this.config.options?.debug) {
2208
+ console.log('🔍 DEBUG: Cancel button clicked');
2209
+ console.log('🔍 DEBUG: this.config.feedback:', this.config.feedback);
2210
+ console.log('🔍 DEBUG: this.config.email:', this.config.email);
2211
+ console.log('🔍 DEBUG: Feedback config exists:', !!(this.config.feedback || this.config.email));
2212
+ }
2213
+
2214
+ // Show feedback form if feedback config exists, otherwise just close
2215
+ if (this.config.feedback || this.config.email) {
2216
+ if (this.config.options?.debug) {
2217
+ console.log('🔍 DEBUG: Showing feedback form');
2218
+ }
2219
+ this.showFeedbackForm();
2220
+ } else {
2221
+ if (this.config.options?.debug) {
2222
+ console.log('🔍 DEBUG: No email config, just closing modal');
2223
+ }
2224
+ modal.classList.remove('show');
2225
+ }
2226
+ });
2227
+
2228
+ // Removed click-outside-to-close behavior to encourage users to use the Cancel button
2229
+ // which triggers the feedback form. This improves feedback collection.
2230
+ }
2231
+
2232
+ private createSuccessModal(): void {
2233
+ if (document.getElementById('download-limiter-success')) return;
2234
+
2235
+ const modal = document.createElement('div');
2236
+ modal.id = 'download-limiter-success';
2237
+ modal.className = 'download-limiter-modal'; // Use same modal class as paywall
2238
+ modal.innerHTML = `
2239
+ <div class="download-limiter-content"> <!-- Use same content class as paywall for theming -->
2240
+ <h3 id="download-limiter-success-title">Action Started!</h3>
2241
+ <p id="download-limiter-success-message">Your action completed successfully!</p>
2242
+ <button id="download-limiter-success-close" class="download-limiter-btn success-btn">
2243
+ OK
2244
+ </button>
2245
+ </div>
2246
+ `;
2247
+ document.body.appendChild(modal);
2248
+
2249
+ // Set up event handlers
2250
+ modal.querySelector('#download-limiter-success-close')!.addEventListener('click', () => {
2251
+ modal.classList.remove('show');
2252
+ });
2253
+
2254
+ modal.addEventListener('click', (e) => {
2255
+ if (e.target === modal) {
2256
+ modal.classList.remove('show');
2257
+ }
2258
+ });
2259
+ }
2260
+
2261
+ private showFeedbackForm(): void {
2262
+ // const feedbackStartTime = Date.now();
2263
+ // console.log(`🕐 [FEEDBACK] showFeedbackForm called: ${new Date(feedbackStartTime).toISOString()}`);
2264
+
2265
+ // Hide paywall first
2266
+ // const paywallHideStart = Date.now();
2267
+ const paywall = document.getElementById('download-limiter-paywall');
2268
+ if (paywall) {
2269
+ paywall.classList.remove('show');
2270
+ // console.log(`🕐 [FEEDBACK] Paywall hidden: ${Date.now() - paywallHideStart}ms`);
2271
+ }
2272
+
2273
+ // Create or show feedback modal
2274
+ // const createFeedbackStart = Date.now();
2275
+ this.createFeedbackModal();
2276
+ // console.log(`🕐 [FEEDBACK] createFeedbackModal completed: ${Date.now() - createFeedbackStart}ms`);
2277
+
2278
+ const modal = document.getElementById('download-limiter-feedback');
2279
+ if (modal) {
2280
+ modal.classList.add('show');
2281
+ // console.log(`🕐 [FEEDBACK] Feedback modal shown: ${Date.now() - feedbackStartTime}ms total`);
2282
+ } else {
2283
+ // console.log(`🕐 [FEEDBACK] ERROR - Feedback modal not found after ${Date.now() - feedbackStartTime}ms`);
2284
+ }
2285
+ }
2286
+
2287
+ private createFeedbackModal(): void {
2288
+ if (document.getElementById('download-limiter-feedback')) {
2289
+ if (this.config.options?.debug) {
2290
+ console.log('🔍 DEBUG: Feedback modal already exists');
2291
+ }
2292
+ return;
2293
+ }
2294
+
2295
+ // Get feedback configuration or fallback to legacy email config
2296
+ const feedbackConfig = this.config.feedback || (this.config.email ? {
2297
+ form: {
2298
+ title: 'Quick Feedback',
2299
+ description: 'Why didn\'t you upgrade?',
2300
+ option1: 'Not useful for my needs',
2301
+ option2: 'Didn\'t find what I was looking for',
2302
+ option3: 'Just trying it out'
2303
+ },
2304
+ email: this.config.email
2305
+ } : null);
2306
+
2307
+ if (this.config.options?.debug) {
2308
+ console.log('🔍 DEBUG: Feedback config:', feedbackConfig);
2309
+ console.log('🔍 DEBUG: this.config.feedback:', this.config.feedback);
2310
+ console.log('🔍 DEBUG: this.config.email:', this.config.email);
2311
+ }
2312
+
2313
+ if (!feedbackConfig) {
2314
+ if (this.config.options?.debug) {
2315
+ console.log('🔍 DEBUG: No feedback config found, not creating modal');
2316
+ }
2317
+ return;
2318
+ }
2319
+
2320
+ const modal = document.createElement('div');
2321
+ modal.id = 'download-limiter-feedback';
2322
+ modal.className = 'download-limiter-modal';
2323
+ modal.innerHTML = `
2324
+ <div class="download-limiter-content">
2325
+ <h3>${feedbackConfig.form.title}</h3>
2326
+ <p style="margin-bottom: 20px;">${feedbackConfig.form.description}</p>
2327
+
2328
+ <form id="feedback-form" style="text-align: left;">
2329
+ <div style="margin-bottom: 15px;">
2330
+ <label style="display: block; margin-bottom: 8px;">
2331
+ <input type="radio" name="reason" value="option1" style="margin-right: 8px;">
2332
+ ${feedbackConfig.form.option1}
2333
+ </label>
2334
+ <label style="display: block; margin-bottom: 8px;">
2335
+ <input type="radio" name="reason" value="option2" style="margin-right: 8px;">
2336
+ ${feedbackConfig.form.option2}
2337
+ </label>
2338
+ <label style="display: block; margin-bottom: 8px;">
2339
+ <input type="radio" name="reason" value="option3" style="margin-right: 8px;">
2340
+ ${feedbackConfig.form.option3}
2341
+ </label>
2342
+ </div>
2343
+
2344
+ <div style="margin-bottom: 15px;">
2345
+ <label for="feedback-details" style="display: block; margin-bottom: 5px;">Tell us more:</label>
2346
+ <textarea
2347
+ id="feedback-details"
2348
+ name="details"
2349
+ rows="3"
2350
+ style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
2351
+ placeholder="Your feedback helps us improve..." maxlength="200"></textarea>
2352
+ </div>
2353
+
2354
+ <div style="margin-bottom: 20px;">
2355
+ <label for="feedback-email" style="display: block; margin-bottom: 5px;">
2356
+ Email (optional):
2357
+ </label>
2358
+ <input
2359
+ type="email"
2360
+ id="feedback-email"
2361
+ name="email"
2362
+ style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
2363
+ placeholder="your@email.com">
2364
+ <small style="color: #666; display: block; margin-top: 4px;">
2365
+ Only if you want us to reach out about what you mentioned above. We don't follow up unless you want us to
2366
+ </small>
2367
+ </div>
2368
+
2369
+
2370
+ <div style="text-align: center;">
2371
+ <button type="submit" class="download-limiter-btn success-btn" style="margin-right: 10px;">
2372
+ Submit Feedback
2373
+ </button>
2374
+ <button type="button" id="feedback-skip-btn" class="download-limiter-btn cancel-btn">
2375
+ Skip
2376
+ </button>
2377
+ </div>
2378
+ </form>
2379
+ </div>
2380
+ `;
2381
+
2382
+ document.body.appendChild(modal);
2383
+
2384
+ if (this.config.options?.debug) {
2385
+ console.log('🔍 DEBUG: Feedback modal created and added to DOM');
2386
+ }
2387
+
2388
+ // Set up event listeners
2389
+ const form = modal.querySelector('#feedback-form') as HTMLFormElement;
2390
+ form.addEventListener('submit', (e) => {
2391
+ e.preventDefault();
2392
+ this.submitFeedback(form);
2393
+ });
2394
+
2395
+ modal.querySelector('#feedback-skip-btn')!.addEventListener('click', () => {
2396
+ modal.classList.remove('show');
2397
+ });
2398
+
2399
+ modal.addEventListener('click', (e) => {
2400
+ if (e.target === modal) {
2401
+ modal.classList.remove('show');
2402
+ }
2403
+ });
2404
+ }
2405
+
2406
+ private async submitFeedback(form: HTMLFormElement): Promise<void> {
2407
+ const formData = new FormData(form);
2408
+ const reason = formData.get('reason') as string;
2409
+ const details = formData.get('details') as string;
2410
+ const email = formData.get('email') as string;
2411
+
2412
+ if (!reason) {
2413
+ alert('Please select a reason');
2414
+ return;
2415
+ }
2416
+
2417
+ const feedbackData = {
2418
+ reason,
2419
+ details,
2420
+ email: email || undefined,
2421
+ timestamp: new Date().toISOString(),
2422
+ userAgent: navigator.userAgent,
2423
+ appId: this.config.appId
2424
+ };
2425
+
2426
+ try {
2427
+ await this.sendFeedbackEmail(feedbackData);
2428
+
2429
+ // Close feedback modal
2430
+ const modal = document.getElementById('download-limiter-feedback');
2431
+ if (modal) {
2432
+ modal.classList.remove('show');
2433
+ }
2434
+
2435
+ // Show thank you message
2436
+ alert('Thank you for your feedback!');
2437
+ } catch (error) {
2438
+ console.error('Failed to send feedback:', error);
2439
+ alert('Failed to send feedback. Please try again.');
2440
+ }
2441
+ }
2442
+
2443
+ private async sendFeedbackEmail(feedbackData: any): Promise<void> {
2444
+ const emailConfig = this.config.feedback?.email || this.config.email;
2445
+ if (!emailConfig) {
2446
+ throw new Error('Email configuration not available');
2447
+ }
2448
+
2449
+ const emailBody = `
2450
+ New Feedback from ${this.config.appId}
2451
+
2452
+ Reason: ${feedbackData.reason}
2453
+ Details: ${feedbackData.details}
2454
+ User Email: ${feedbackData.email || 'Not provided'}
2455
+ Timestamp: ${feedbackData.timestamp}
2456
+ User Agent: ${feedbackData.userAgent}
2457
+ `.trim();
2458
+
2459
+ const response = await fetch('https://api.resend.com/emails', {
2460
+ method: 'POST',
2461
+ headers: {
2462
+ 'Content-Type': 'application/json',
2463
+ 'Authorization': `Bearer ${emailConfig.resendApiKey}`
2464
+ },
2465
+ body: JSON.stringify({
2466
+ from: emailConfig.fromEmail,
2467
+ to: [emailConfig.fromEmail], // Send feedback to yourself
2468
+ subject: `Feedback from ${this.config.appId}`,
2469
+ text: emailBody
2470
+ })
2471
+ });
2472
+
2473
+ if (!response.ok) {
2474
+ throw new Error(`Email API error: ${response.status}`);
2475
+ }
2476
+ }
2477
+
2478
+ private createAuthUI(): void {
2479
+ if (document.getElementById('lead-fast-profile')) return;
2480
+
2481
+ const profileContainer = document.createElement('div');
2482
+ profileContainer.id = 'lead-fast-profile';
2483
+ profileContainer.className = 'lead-fast-profile';
2484
+ document.body.appendChild(profileContainer);
2485
+ }
2486
+
2487
+ private createStatusDisplay(): void {
2488
+ if (document.getElementById('download-limiter-status')) return;
2489
+
2490
+ const statusDiv = document.createElement('div');
2491
+ statusDiv.id = 'download-limiter-status';
2492
+ statusDiv.className = 'download-limiter-status';
2493
+ document.body.appendChild(statusDiv);
2494
+ }
2495
+
2496
+ private createGlobalStatusDisplay(): void {
2497
+ this.createStatusDisplay();
2498
+ }
2499
+
2500
+
2501
+ private showPaywallInstant(status: DownloadStatus): void {
2502
+ // const showStartTime = Date.now();
2503
+ // console.log(`🕐 [MODAL] showPaywallInstant started: ${new Date(showStartTime).toISOString()}`);
2504
+
2505
+ // Ensure modal exists before trying to show it
2506
+ // const createStartTime = Date.now();
2507
+ this.createPaywallModal();
2508
+ // console.log(`🕐 [MODAL] createPaywallModal completed: ${Date.now() - createStartTime}ms`);
2509
+
2510
+ const modal = document.getElementById('download-limiter-paywall');
2511
+ if (modal) {
2512
+ // console.log(`🕐 [MODAL] Modal found in DOM: ${Date.now() - showStartTime}ms`);
2513
+
2514
+ // Update paywall title with exact count (no API call needed)
2515
+ const titleEl = modal.querySelector('#download-limiter-paywall-title');
2516
+ if (titleEl) {
2517
+ titleEl.textContent = `You've used ${status.limit} free attempts`;
2518
+ // console.log(`🕐 [MODAL] Title updated: ${Date.now() - showStartTime}ms`);
2519
+ }
2520
+
2521
+ // Show appropriate section based on auth state
2522
+ const authSection = modal.querySelector('#download-limiter-auth-section') as HTMLElement;
2523
+ const paymentSection = modal.querySelector('#download-limiter-payment-section') as HTMLElement;
2524
+
2525
+ if (!this.currentUser) {
2526
+ // console.log(`🕐 [MODAL] Setting up anonymous user flow: ${Date.now() - showStartTime}ms`);
2527
+ // Anonymous user - show single upgrade flow
2528
+ const subtitleEl = modal.querySelector('#download-limiter-paywall-subtitle');
2529
+ if (subtitleEl) {
2530
+ subtitleEl.textContent = 'Sign in to continue. If you\'re already premium, you\'ll get unlimited access. New users can upgrade after signing in.';
2531
+ }
2532
+
2533
+ // Hide separate auth/payment sections and show single upgrade button
2534
+ authSection.style.display = 'none';
2535
+ paymentSection.style.display = 'none';
2536
+
2537
+ // Show single upgrade button
2538
+ // const upgradeStartTime = Date.now();
2539
+ this.showSingleUpgradeFlow(modal);
2540
+ // console.log(`🕐 [MODAL] Single upgrade flow setup: ${Date.now() - upgradeStartTime}ms`);
2541
+ } else {
2542
+ // console.log(`🕐 [MODAL] Setting up signed-in user flow: ${Date.now() - showStartTime}ms`);
2543
+ // Existing flow for signed-in users - direct to payment
2544
+ authSection.style.display = 'none';
2545
+ paymentSection.style.display = 'block';
2546
+ }
2547
+
2548
+ modal.classList.add('show');
2549
+ // console.log(`🕐 [MODAL] Modal shown (classList.add('show')): ${Date.now() - showStartTime}ms`);
2550
+ } else {
2551
+ // console.log(`🕐 [MODAL] ERROR: Modal not found in DOM after creation: ${Date.now() - showStartTime}ms`);
2552
+ }
2553
+ }
2554
+
2555
+ private async showPaywall(): Promise<void> {
2556
+ // Get current status if not cached
2557
+ const status = await this.getDownloadStatus();
2558
+ this.showPaywallInstant(status);
2559
+ }
2560
+
2561
+ private showSingleUpgradeFlow(modal: HTMLElement): void {
2562
+ // Create single upgrade button that handles both login and payment
2563
+ let upgradeContainer = modal.querySelector('.single-upgrade-container') as HTMLElement;
2564
+
2565
+ if (!upgradeContainer) {
2566
+ upgradeContainer = document.createElement('div');
2567
+ upgradeContainer.className = 'single-upgrade-container';
2568
+ upgradeContainer.innerHTML = `
2569
+ <div class="single-upgrade-content">
2570
+ <button class="download-limiter-btn upgrade-btn" id="single-upgrade-btn">
2571
+ Continue with Google
2572
+ </button>
2573
+ <!--<p class="upgrade-description">
2574
+ Sign in to restore access. Premium members get unlimited usage immediately.
2575
+ </p>-->
2576
+ </div>
2577
+ `;
2578
+ // Insert before the cancel container to keep cancel at the bottom
2579
+ const cancelContainer = modal.querySelector('.download-limiter-cancel-container');
2580
+ if (cancelContainer) {
2581
+ modal.querySelector('.download-limiter-content')?.insertBefore(upgradeContainer, cancelContainer);
2582
+ } else {
2583
+ modal.querySelector('.download-limiter-content')?.appendChild(upgradeContainer);
2584
+ }
2585
+ }
2586
+
2587
+ upgradeContainer.style.display = 'block';
2588
+
2589
+ // Add click handler for single upgrade flow
2590
+ const upgradeBtn = modal.querySelector('#single-upgrade-btn');
2591
+ if (upgradeBtn) {
2592
+ upgradeBtn.replaceWith(upgradeBtn.cloneNode(true)); // Remove existing listeners
2593
+ const newUpgradeBtn = modal.querySelector('#single-upgrade-btn');
2594
+
2595
+ newUpgradeBtn?.addEventListener('click', async () => {
2596
+ try {
2597
+ // Step 1: Sign in with Google
2598
+ const { error } = await this.supabase.auth.signInWithOAuth({
2599
+ provider: 'google',
2600
+ options: {
2601
+ redirectTo: `${window.location.origin}${window.location.pathname}?upgrade=true`
2602
+ }
2603
+ });
2604
+
2605
+ if (error) {
2606
+ console.error('Sign in error:', error);
2607
+ this.showError('Sign In Failed', 'Failed to sign in with Google. Please try again.');
2608
+ }
2609
+ // OAuth will redirect, so no further action needed here
2610
+ } catch (error) {
2611
+ console.error('Upgrade flow error:', error);
2612
+ this.showError('Upgrade Failed', 'Failed to start upgrade process. Please try again.');
2613
+ }
2614
+ });
2615
+ }
2616
+ }
2617
+
2618
+
2619
+ private showSuccess(title: string, message: string): void {
2620
+ const modal = document.getElementById('download-limiter-success');
2621
+ if (modal) {
2622
+ const titleEl = modal.querySelector('#download-limiter-success-title');
2623
+ const messageEl = modal.querySelector('#download-limiter-success-message');
2624
+
2625
+ if (titleEl) titleEl.textContent = title;
2626
+ if (messageEl) messageEl.textContent = message;
2627
+
2628
+ modal.classList.add('show');
2629
+
2630
+ // No auto-hide - let user dismiss manually
2631
+ }
2632
+ }
2633
+
2634
+ private showError(title: string, message: string): void {
2635
+ // Create a clean error toast instead of alert
2636
+ const toast = document.createElement('div');
2637
+ toast.style.cssText = `
2638
+ position: fixed;
2639
+ top: 20px;
2640
+ right: 20px;
2641
+ background: #fee2e2;
2642
+ border: 1px solid #fecaca;
2643
+ color: #dc2626;
2644
+ padding: 12px 16px;
2645
+ border-radius: 8px;
2646
+ font-size: 14px;
2647
+ font-weight: 500;
2648
+ line-height: 1.4;
2649
+ max-width: 300px;
2650
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
2651
+ z-index: 10000;
2652
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2653
+ `;
2654
+
2655
+ toast.innerHTML = `
2656
+ <div style="font-weight: 600; margin-bottom: 4px;">${title}</div>
2657
+ <div style="font-size: 13px; opacity: 0.9;">${message}</div>
2658
+ `;
2659
+
2660
+ document.body.appendChild(toast);
2661
+
2662
+ // Auto-remove after 5 seconds
2663
+ setTimeout(() => {
2664
+ if (toast.parentNode) {
2665
+ toast.parentNode.removeChild(toast);
2666
+ }
2667
+ }, 5000);
2668
+
2669
+ // Click to dismiss
2670
+ toast.addEventListener('click', () => {
2671
+ if (toast.parentNode) {
2672
+ toast.parentNode.removeChild(toast);
2673
+ }
2674
+ });
2675
+ }
2676
+
2677
+ private showConnectionError(): void {
2678
+ this.showError(
2679
+ 'Connection Error',
2680
+ 'Server connection required for download verification. Please check your configuration and try again.'
2681
+ );
2682
+ }
2683
+
2684
+ private showPaymentConfigError(): void {
2685
+ this.showError(
2686
+ 'Payment Configuration Error',
2687
+ 'Payment system is not properly configured. Please contact support.'
2688
+ );
2689
+ }
2690
+
2691
+ private disableDownloadButtons(errorType: 'connection' | 'payment'): void {
2692
+ // Find all buttons that were attached by the library
2693
+ const buttons = document.querySelectorAll('[data-leadfast-attached]');
2694
+
2695
+ const errorMessages = {
2696
+ connection: 'Download disabled: Server connection required',
2697
+ payment: 'Download disabled: Payment configuration error'
2698
+ };
2699
+
2700
+ const statusMessages = {
2701
+ connection: '⚠️ Connection Error - Downloads Disabled',
2702
+ payment: '⚠️ Payment Config Error - Downloads Disabled'
2703
+ };
2704
+
2705
+ buttons.forEach(button => {
2706
+ const btnElement = button as HTMLButtonElement;
2707
+ btnElement.disabled = true;
2708
+ btnElement.style.opacity = '0.5';
2709
+ btnElement.style.cursor = 'not-allowed';
2710
+ btnElement.title = errorMessages[errorType];
2711
+ });
2712
+
2713
+ // Update user section to show error
2714
+ const userSection = document.querySelector('.lead-fast-user-section');
2715
+ if (userSection) {
2716
+ userSection.innerHTML = `
2717
+ <div style="
2718
+ color: #ef4444;
2719
+ padding: 6px 8px;
2720
+ font-size: 12px;
2721
+ font-weight: 500;
2722
+ line-height: 1.2;
2723
+ border-radius: 4px;
2724
+ background: rgba(239, 68, 68, 0.05);
2725
+ border: 1px solid rgba(239, 68, 68, 0.2);
2726
+ max-width: 200px;
2727
+ text-align: center;
2728
+ ">
2729
+ ${statusMessages[errorType]}
2730
+ </div>
2731
+ `;
2732
+ }
2733
+ }
2734
+
2735
+ private async updateStatusDisplays(): Promise<void> {
2736
+ //console.log('🎨 [THEME DEBUG] updateStatusDisplays called');
2737
+
2738
+ // If connection failed, disable all download buttons
2739
+ if (this.supabaseConnectionFailed) {
2740
+ this.disableDownloadButtons('connection');
2741
+ return;
2742
+ }
2743
+
2744
+ // If payment configuration is invalid, disable all download buttons
2745
+ if (this.config.payment && this.paymentConnectionFailed) {
2746
+ this.disableDownloadButtons('payment');
2747
+ return;
2748
+ }
2749
+
2750
+ const status = await this.getDownloadStatus();
2751
+ this.updateUserSection(status);
2752
+ }
2753
+
2754
+ private createTitleBar(): void {
2755
+ //console.log('🎨 [THEME DEBUG] Creating title bar');
2756
+
2757
+ // Create the profile div if it doesn't exist
2758
+ let profileDiv = document.getElementById('lead-fast-profile');
2759
+ if (!profileDiv) {
2760
+ profileDiv = document.createElement('div');
2761
+ profileDiv.id = 'lead-fast-profile';
2762
+ profileDiv.className = 'lead-fast-profile';
2763
+ document.body.appendChild(profileDiv);
2764
+ }
2765
+
2766
+ // Get title bar configuration
2767
+ const titleConfig = this.config.titleBar || {};
2768
+ const title = titleConfig.title || 'My App';
2769
+ const titleImage = titleConfig.titleImage;
2770
+ const links = titleConfig.links || [];
2771
+
2772
+ // Always create title bar with logo and navigation
2773
+ profileDiv.innerHTML = `
2774
+ <div class="lead-fast-title-bar">
2775
+ <div class="lead-fast-title-section">
2776
+ <a href="#" class="lead-fast-logo">
2777
+ ${titleImage ? `<img src="${titleImage}" alt="${title}">` : ''}
2778
+ <span>${title}</span>
2779
+ </a>
2780
+ <nav class="lead-fast-nav">
2781
+ ${links.map(link => `
2782
+ <a href="${link.url}" class="lead-fast-nav-link" ${link.target ? `target="${link.target}"` : ''}>
2783
+ ${link.text}
2784
+ </a>
2785
+ `).join('')}
2786
+ </nav>
2787
+ </div>
2788
+
2789
+ <div class="lead-fast-user-section" id="lead-fast-user-section">
2790
+ <!-- User section will be populated when logged in -->
2791
+ </div>
2792
+
2793
+ <!-- <a href="https://www.moneyfast.bar" target="_blank" class="lead-fast-branding" style="text-decoration: none; color: inherit;">moneyfast.bar</a>-->
2794
+ </div>
2795
+ `;
2796
+ }
2797
+
2798
+ private updateUserSection(status: DownloadStatus): void {
2799
+ //console.log('🎨 [THEME DEBUG] updateUserSection called');
2800
+ //console.log('🎨 [THEME DEBUG] Current data-theme:', document.documentElement.getAttribute('data-theme'));
2801
+
2802
+ const userSection = document.getElementById('lead-fast-user-section');
2803
+ if (!userSection) {
2804
+ //console.log('🎨 [THEME DEBUG] User section not found!');
2805
+ return;
2806
+ }
2807
+
2808
+ if (this.currentUser) {
2809
+ // User is signed in - show user info
2810
+ const userInitial = this.currentUser.email.charAt(0).toUpperCase();
2811
+ const userEmail = this.currentUser.email;
2812
+ const badgeClass = status.isPremium ? 'premium' : 'free';
2813
+ const badgeText = status.isPremium ? 'Premium' : 'Free';
2814
+ const countText = status.isPremium ? 'Unlimited' : `${status.currentCount}/${status.limit}`;
2815
+
2816
+ //console.log('🎨 [THEME DEBUG] Populating user section');
2817
+
2818
+ userSection.innerHTML = `
2819
+ <div class="lead-fast-avatar">${userInitial}</div>
2820
+ <div class="lead-fast-user-info">
2821
+ <div class="lead-fast-user-email">${userEmail}</div>
2822
+ <div class="lead-fast-user-status">
2823
+ <span class="lead-fast-badge ${badgeClass}">${badgeText}</span>
2824
+ <span class="lead-fast-count">${countText}</span>
2825
+ </div>
2826
+ </div>
2827
+ <button class="lead-fast-signout">signout</button>
2828
+ `;
2829
+
2830
+ // Debug CSS variables after updating user section
2831
+ const titleBar = document.querySelector('.lead-fast-title-bar') as HTMLElement;
2832
+ if (titleBar) {
2833
+ const computedStyle = getComputedStyle(titleBar);
2834
+ //console.log('🎨 [THEME DEBUG] Title bar background color:', computedStyle.backgroundColor);
2835
+ //console.log('🎨 [THEME DEBUG] Title bar border color:', computedStyle.borderColor);
2836
+ }
2837
+
2838
+ // Add sign out functionality
2839
+ const signoutBtn = userSection.querySelector('.lead-fast-signout');
2840
+ if (signoutBtn) {
2841
+ signoutBtn.addEventListener('click', () => {
2842
+ this.signOut();
2843
+ });
2844
+ }
2845
+ } else {
2846
+ // Anonymous user - show usage count
2847
+ //console.log('🎨 [THEME DEBUG] Showing anonymous user usage count');
2848
+ const countText = `Attempts: ${status.currentCount}/${status.limit}`;
2849
+ userSection.innerHTML = `
2850
+ <div class="lead-fast-anonymous-status">
2851
+ <span class="lead-fast-count">${countText}</span>
2852
+ </div>
2853
+ `;
2854
+ }
2855
+ }
2856
+
2857
+ private setupUIUpdates(): void {
2858
+ // Update UI on auth changes
2859
+ this.on('authChanged', async () => {
2860
+ // Invalidate cache when auth state changes
2861
+ // console.log(`🕐 [CACHE] Invalidating cache due to auth change (was: ${this.cachedStatus ? 'cached' : 'null'})`);
2862
+ this.cachedStatus = null;
2863
+ this.updateStatusDisplays();
2864
+
2865
+ // Update paywall display if it's currently shown
2866
+ const modal = document.getElementById('download-limiter-paywall');
2867
+ if (modal && modal.classList.contains('show')) {
2868
+ this.updatePaywallDisplay();
2869
+
2870
+ // Auto-redirect to payment after sign-in from paywall (if not already premium)
2871
+ if (this.currentUser && this.config.payment) {
2872
+ setTimeout(async () => {
2873
+ const isPremium = await this.checkPremiumStatus(this.currentUser.email);
2874
+ if (!isPremium) {
2875
+ // User is not premium, redirect to payment
2876
+ const payment = await this.createPayment();
2877
+ if (payment?.checkout_url) {
2878
+ window.location.href = payment.checkout_url;
2879
+ }
2880
+ } else {
2881
+ // User is already premium, just close paywall and stay on page
2882
+ modal.classList.remove('show');
2883
+ }
2884
+ }, 1000); // Small delay to let UI update
2885
+ }
2886
+ }
2887
+ });
2888
+
2889
+ // Update UI on count changes
2890
+ this.on('countChanged', () => {
2891
+ this.updateStatusDisplays();
2892
+ });
2893
+
2894
+ // Initial update
2895
+ this.updateStatusDisplays();
2896
+ }
2897
+
2898
+ private updatePaywallDisplay(): void {
2899
+ const modal = document.getElementById('download-limiter-paywall');
2900
+ if (!modal) return;
2901
+
2902
+ const authSection = modal.querySelector('#download-limiter-auth-section') as HTMLElement;
2903
+ const paymentSection = modal.querySelector('#download-limiter-payment-section') as HTMLElement;
2904
+
2905
+ if (!this.currentUser) {
2906
+ // Not signed in - show auth section
2907
+ authSection.style.display = 'block';
2908
+ paymentSection.style.display = 'none';
2909
+ } else {
2910
+ // Signed in - show payment section
2911
+ authSection.style.display = 'none';
2912
+ paymentSection.style.display = 'block';
2913
+ }
2914
+ }
2915
+
2916
+ /**
2917
+ * Event listener management
2918
+ */
2919
+ public on<K extends keyof MoneyBarEvents>(
2920
+ event: K,
2921
+ listener: (data: MoneyBarEvents[K]) => void
2922
+ ): void {
2923
+ if (!this.eventListeners.has(event)) {
2924
+ this.eventListeners.set(event, []);
2925
+ }
2926
+ this.eventListeners.get(event)!.push(listener);
2927
+ }
2928
+
2929
+ public off<K extends keyof MoneyBarEvents>(
2930
+ event: K,
2931
+ listener: (data: MoneyBarEvents[K]) => void
2932
+ ): void {
2933
+ const listeners = this.eventListeners.get(event);
2934
+ if (listeners) {
2935
+ const index = listeners.indexOf(listener);
2936
+ if (index > -1) {
2937
+ listeners.splice(index, 1);
2938
+ }
2939
+ }
2940
+ }
2941
+
2942
+ private emit<K extends keyof MoneyBarEvents>(
2943
+ event: K,
2944
+ data: MoneyBarEvents[K]
2945
+ ): void {
2946
+ const listeners = this.eventListeners.get(event);
2947
+ if (listeners) {
2948
+ listeners.forEach(listener => {
2949
+ try {
2950
+ listener(data);
2951
+ } catch (error) {
2952
+ console.error(`Error in ${event} listener:`, error);
2953
+ }
2954
+ });
2955
+ }
2956
+ }
2957
+
2958
+ private validateConfig(): void {
2959
+ if (!this.config.appId) throw new Error('appId is required');
2960
+ if (!this.config.supabase?.url) throw new Error('supabase.url is required');
2961
+ if (!this.config.supabase?.anonKey) throw new Error('supabase.anonKey is required');
2962
+ if (typeof this.config.freeDownloadLimit !== 'number' || this.config.freeDownloadLimit < 0) {
2963
+ throw new Error('freeDownloadLimit must be a non-negative number');
2964
+ }
2965
+ }
2966
+
2967
+ private async initializeSupabase(): Promise<void> {
2968
+ try {
2969
+ this.supabase = createClient(
2970
+ this.config.supabase.url,
2971
+ this.config.supabase.anonKey
2972
+ );
2973
+
2974
+ // Skip connection test - we'll detect connection issues during actual API calls
2975
+ this.supabaseConnectionFailed = false;
2976
+ } catch (error) {
2977
+ console.error('Failed to initialize Supabase:', error);
2978
+ this.supabaseConnectionFailed = true;
2979
+ }
2980
+ }
2981
+
2982
+ private async initializePayment(): Promise<void> {
2983
+ // Skip if no payment configuration
2984
+ if (!this.config.payment) {
2985
+ return;
2986
+ }
2987
+
2988
+ // Validate product ID format
2989
+ const productId = this.config.payment.productId;
2990
+ if (!productId || !productId.startsWith('pdt_') || productId.length < 10) {
2991
+ this.paymentConnectionFailed = true;
2992
+ if (this.config.options?.debug) {
2993
+ console.warn('Invalid payment product ID format:', productId);
2994
+ }
2995
+ return;
2996
+ }
2997
+
2998
+ // For now, skip payment gateway connectivity test
2999
+ // Only validate product ID format (which we already did above)
3000
+ // In production, you can add actual DodoPayments API test here
3001
+ this.paymentConnectionFailed = false;
3002
+ }
3003
+
3004
+ private async initializeFingerprint(): Promise<void> {
3005
+ try {
3006
+ this.userFingerprint = await this.generateFingerprint();
3007
+ } catch (error) {
3008
+ this.userFingerprint = crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
3009
+ if (this.config.options?.debug) {
3010
+ console.warn('Fingerprint generation failed, using fallback:', error);
3011
+ }
3012
+ }
3013
+ }
3014
+
3015
+ private async generateFingerprint(): Promise<string> {
3016
+ const ua = navigator.userAgent || '';
3017
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
3018
+ const screenSize = `${screen.width}x${screen.height}`;
3019
+ const lang = navigator.language || '';
3020
+ const raw = `${ua}||${tz}||${screenSize}||${lang}`;
3021
+
3022
+ const buf = new TextEncoder().encode(raw);
3023
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
3024
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
3025
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
3026
+ }
3027
+
3028
+ private async setupAuthListener(): Promise<void> {
3029
+ this.supabase.auth.onAuthStateChange(async (event: string, session: any) => {
3030
+ const previousUser = this.currentUser;
3031
+ this.currentUser = session?.user || null;
3032
+
3033
+ let isPremium = false;
3034
+ if (this.currentUser) {
3035
+ // Only check premium status if user changed or cache is expired
3036
+ const userChanged = !previousUser || previousUser.email !== this.currentUser.email;
3037
+ const cacheExpired = !this.cachedPremiumStatus || (Date.now() - this.premiumStatusCacheTime) > this.STATUS_CACHE_DURATION;
3038
+
3039
+ if (userChanged || cacheExpired) {
3040
+ console.log(`🔥 [SUPABASE API] About to call checkPremiumStatus from setupAuthListener (userChanged: ${userChanged}, cacheExpired: ${cacheExpired})`);
3041
+ isPremium = await this.checkPremiumStatus(this.currentUser.email);
3042
+ // Cache the result
3043
+ this.cachedPremiumStatus = isPremium;
3044
+ this.premiumStatusCacheTime = Date.now();
3045
+ } else {
3046
+ // Use cached result
3047
+ isPremium = this.cachedPremiumStatus!; // We know it's not null because cacheExpired is false
3048
+ console.log(`🔍 [CACHE] Using cached premium status from auth listener: ${isPremium}`);
3049
+ }
3050
+ } else {
3051
+ // Clear cache when user logs out
3052
+ this.cachedPremiumStatus = null;
3053
+ this.premiumStatusCacheTime = 0;
3054
+ }
3055
+
3056
+ // Check if user just signed in for upgrade flow
3057
+ const urlParams = new URLSearchParams(window.location.search);
3058
+ const isUpgradeFlow = urlParams.get('upgrade') === 'true';
3059
+
3060
+ if (event === 'SIGNED_IN' && !previousUser && this.currentUser && this.config.payment) {
3061
+ // Check if this is an upgrade flow (from anonymous trial)
3062
+ if (isUpgradeFlow) {
3063
+ if (!isPremium) {
3064
+ // User signed in for upgrade - redirect to payment immediately
3065
+ const payment = await this.createPayment();
3066
+ if (payment?.checkout_url) {
3067
+ // Clean URL before redirect
3068
+ window.history.replaceState({}, document.title, window.location.pathname);
3069
+ window.location.href = payment.checkout_url;
3070
+ }
3071
+ } else {
3072
+ // User is already premium, just clean URL and close modal
3073
+ window.history.replaceState({}, document.title, window.location.pathname);
3074
+ const modal = document.getElementById('download-limiter-paywall');
3075
+ if (modal) {
3076
+ modal.classList.remove('show');
3077
+ }
3078
+ }
3079
+ }
3080
+ }
3081
+
3082
+ this.emit('authChanged', { user: this.currentUser, isPremium });
3083
+ });
3084
+
3085
+ // Get initial auth state
3086
+ const { data: { user } } = await this.supabase.auth.getUser();
3087
+ this.currentUser = user;
3088
+ }
3089
+
3090
+ private async getDownloadCount(): Promise<number> {
3091
+ if (!this.userFingerprint) return 0;
3092
+
3093
+ console.log(`🔥 [SUPABASE API] getDownloadCount called | Time: ${new Date().toISOString()}`);
3094
+ try {
3095
+ const response = await fetch(`${this.config.supabase.url}/functions/v1/check-download-limit`, {
3096
+ method: 'POST',
3097
+ headers: {
3098
+ 'Content-Type': 'application/json',
3099
+ 'Authorization': `Bearer ${this.config.supabase.anonKey}`
3100
+ },
3101
+ body: JSON.stringify({
3102
+ fingerprint: this.userFingerprint,
3103
+ app_id: this.config.appId,
3104
+ action: 'check'
3105
+ })
3106
+ });
3107
+ console.log(`🔥 [SUPABASE API] getDownloadCount response: ${response.status}`);
3108
+
3109
+ if (response.ok) {
3110
+ const data = await response.json();
3111
+ this.supabaseConnectionFailed = false; // Connection successful
3112
+ return data.current_count || 0;
3113
+ } else {
3114
+ // Handle auth errors (401/403) as connection failures
3115
+ if (response.status === 401 || response.status === 403) {
3116
+ console.warn(`Supabase authentication failed: ${response.status} - Invalid anon key`);
3117
+ this.supabaseConnectionFailed = true;
3118
+ }
3119
+ }
3120
+ } catch (error) {
3121
+ this.supabaseConnectionFailed = true; // Mark connection as failed
3122
+ if (this.config.options?.debug) {
3123
+ console.warn('Failed to get server usage count, connection failed:', error);
3124
+ }
3125
+ }
3126
+
3127
+ // Security: Do not fallback to localStorage when server connection fails
3128
+ // This prevents users from bypassing limits by manipulating localStorage
3129
+ return 0;
3130
+ }
3131
+
3132
+ private async incrementDownloadCount(): Promise<number> {
3133
+ if (!this.userFingerprint) {
3134
+ // if (this.config.options?.debug) {
3135
+ // console.warn('🔍 DEBUG: No userFingerprint available for incrementDownloadCount');
3136
+ // }
3137
+ return 0;
3138
+ }
3139
+
3140
+ const requestBody = {
3141
+ fingerprint: this.userFingerprint,
3142
+ app_id: this.config.appId,
3143
+ action: 'increment'
3144
+ };
3145
+
3146
+ console.log(`🔥 [SUPABASE API] incrementDownloadCount called | Time: ${new Date().toISOString()}`);
3147
+
3148
+ try {
3149
+ const response = await fetch(`${this.config.supabase.url}/functions/v1/check-download-limit`, {
3150
+ method: 'POST',
3151
+ headers: {
3152
+ 'Content-Type': 'application/json',
3153
+ 'Authorization': `Bearer ${this.config.supabase.anonKey}`
3154
+ },
3155
+ body: JSON.stringify(requestBody)
3156
+ });
3157
+ console.log(`🔥 [SUPABASE API] incrementDownloadCount response: ${response.status}`);
3158
+
3159
+ if (this.config.options?.debug) {
3160
+ //console.log('🔍 DEBUG: Response status:', response.status);
3161
+ if (!response.ok) {
3162
+ const errorText = await response.text();
3163
+ console.error('🔍 DEBUG: Error response:', errorText);
3164
+ }
3165
+ }
3166
+
3167
+ if (response.ok) {
3168
+ const data = await response.json();
3169
+ this.supabaseConnectionFailed = false; // Connection successful
3170
+ return data.new_count || 0;
3171
+ } else {
3172
+ // Handle auth errors (401/403) as connection failures
3173
+ if (response.status === 401 || response.status === 403) {
3174
+ console.warn(`Supabase authentication failed: ${response.status} - Invalid anon key`);
3175
+ this.supabaseConnectionFailed = true;
3176
+ }
3177
+ }
3178
+ } catch (error) {
3179
+ this.supabaseConnectionFailed = true; // Mark connection as failed
3180
+ if (this.config.options?.debug) {
3181
+ console.warn('Failed to increment server count, connection failed:', error);
3182
+ }
3183
+ }
3184
+
3185
+ // Security: Do not fallback to localStorage when server connection fails
3186
+ // This prevents users from bypassing limits by manipulating localStorage
3187
+ return 0;
3188
+ }
3189
+
3190
+ private async checkPremiumStatus(email: string): Promise<boolean> {
3191
+ console.log(`🔥 [SUPABASE API] checkPremiumStatus called for email: ${email} | Time: ${new Date().toISOString()}`);
3192
+ try {
3193
+ const response = await fetch(`${this.config.supabase.url}/functions/v1/check-payment-status`, {
3194
+ method: 'POST',
3195
+ headers: {
3196
+ 'Content-Type': 'application/json',
3197
+ 'Authorization': `Bearer ${this.config.supabase.anonKey}`
3198
+ },
3199
+ body: JSON.stringify({
3200
+ email,
3201
+ app_id: this.config.appId
3202
+ })
3203
+ });
3204
+ console.log(`🔥 [SUPABASE API] checkPremiumStatus response: ${response.status}`);
3205
+
3206
+ if (response.ok) {
3207
+ const data = await response.json();
3208
+ return data.status === 'completed';
3209
+ }
3210
+ } catch (error) {
3211
+ if (this.config.options?.debug) {
3212
+ console.warn('Failed to check premium status:', error);
3213
+ }
3214
+ }
3215
+
3216
+ return false;
3217
+ }
3218
+
3219
+ private getLocalDownloadCount(): number {
3220
+ const key = `download_count_${this.config.appId}`;
3221
+ const val = parseInt(localStorage.getItem(key) || '0', 10);
3222
+ return isNaN(val) ? 0 : val;
3223
+ }
3224
+
3225
+ private incrementLocalDownloadCount(): number {
3226
+ const key = `download_count_${this.config.appId}`;
3227
+ const next = this.getLocalDownloadCount() + 1;
3228
+ localStorage.setItem(key, String(next));
3229
+ return next;
3230
+ }
3231
+
3232
+ private handleError(error: Error): void {
3233
+ if (this.config.options?.debug) {
3234
+ console.error('Lead Fast Error:', error);
3235
+ }
3236
+
3237
+ if (this.config.callbacks?.onError) {
3238
+ this.config.callbacks.onError(error);
3239
+ } else {
3240
+ // Default error handling
3241
+ console.error('Lead Fast:', error.message);
3242
+ }
3243
+
3244
+ this.emit('error', error);
3245
+ }
3246
+
3247
+ private async checkPaymentStatus(): Promise<void> {
3248
+ // Check if user returned from payment
3249
+ const urlParams = new URLSearchParams(window.location.search);
3250
+ if (urlParams.get('payment') === 'success') {
3251
+ // Wait for auth to be ready
3252
+ setTimeout(async () => {
3253
+ if (this.currentUser) {
3254
+ const isPremium = await this.checkPremiumStatus(this.currentUser.email);
3255
+ if (isPremium) {
3256
+ this.showSuccess('Payment Successful!', 'Welcome to premium! You now have unlimited usage.');
3257
+ } else {
3258
+ this.showSuccess('Processing Payment', 'Payment is being processed. Please refresh in a few moments.');
3259
+ }
3260
+ } else {
3261
+ this.showSuccess('Processing Payment', 'Please wait while we verify your payment...');
3262
+ }
3263
+
3264
+ // Clean up URL
3265
+ const cleanUrl = window.location.href.split('?')[0];
3266
+ window.history.replaceState({}, document.title, cleanUrl);
3267
+ }, 2000);
3268
+ }
3269
+ }
3270
+ }
3271
+
3272
+ // Export types for consumer use
3273
+ export * from './types.js';