PageSourceSearch

https://vibeotter-5690a.firebaseapp.com/js/subscription-status.js?v=prompts-20260710

js vibeotter-5690a.firebaseapp.com collected 2026-10-03 14:08:27 UTC 20,082 bytes, 528 lines download raw bytes

1// Customer-facing usage is called Prompt/Prompts. Chat-named API fields, DOM hooks,
2// functions, and storage keys below are intentionally retained as compatibility contracts.
3// Check and display user's subscription status
4async function checkSubscriptionStatus() {
5    try {
6        const user = firebase.auth().currentUser;
7        if (!user) {
8            return null;
9        }
10
11        // Builder usage belongs to the site owner. Wait until the session role is
12        // known, then keep account subscription controls out of an editor's view.
13        if (isBuilderPage() && !window.sessionData) return null;
14        if (isSharedSiteEditor()) {
15            clearSharedEditorSubscriptionUI();
16            showSharedEditorPromptBadge();
17            return null;
18        }
19
20        const apiUrl = window.APP_CONFIG ? window.APP_CONFIG.getApiUrl() : 'http://localhost:3000';
21        const subHeaders = {
22            'Authorization': `Bearer ${await user.getIdToken()}`
23        };
24
25        // Include designer signup flag if present (designers don't receive marketing emails)
26        if (sessionStorage.getItem('vibeotter_designer_signup') === 'true') {
27            subHeaders['X-Designer-Signup'] = 'true';
28        }
29
30        const response = await fetch(`${apiUrl}/api/stripe-simple/check-subscription`, {
31            headers: subHeaders
32        });
33
34        const data = await response.json();
35        
36        if (data.subscription) {
37            const sub = data.subscription;
38
39            // Update UI to show subscription status
40            updateSubscriptionUI(sub);
41
42            // Store in local state for quick access
43            window.currentSubscription = sub;
44
45            // Cache per-site subscription data for dashboard (multi-site support).
46            // Always replaced wholesale: a response without the array means no
47            // per-site rows, never "keep the previous user's data".
48            window.siteSubscriptions = Array.isArray(data.siteSubscriptions) ? data.siteSubscriptions : [];
49
50            maybeShowFreeChatWarning(sub);
51
52            if (typeof window.refreshFirstSiteOfferStatus === 'function') {
53                window.refreshFirstSiteOfferStatus();
54            }
55        }
56
57        return data;
58    } catch (error) {
59        console.error('Failed to check subscription status:', error);
60        return null;
61    }
62}
63
64function isBuilderPage() {
65    return window.location.pathname.includes('builder.html') ||
66        window.location.pathname.endsWith('builder') ||
67        !!document.querySelector('.builder-container');
68}
69
70function isSharedSiteEditor() {
71    return isBuilderPage() && window.sessionData?.accessRole === 'editor';
72}
73
74function clearSharedEditorSubscriptionUI() {
75    const badge = document.querySelector('.subscription-badge');
76    if (badge) badge.remove();
77    closeFreeChatWarningPopover();
78    window.currentSubscription = null;
79    window.siteSubscriptions = [];
80}
81
82// Read-only Prompt badge for shared-site editors. Editors have no subscription
83// controls of their own; this shows the OWNER's allowance their Prompts consume
84// (data from the session payload's ownerPromptUsage, set only for editors in
85// sessions.js GET /:id). Non-clickable — no paywall or portal wiring. Owners never
86// reach this: it is gated by isSharedSiteEditor() and only called right after
87// clearSharedEditorSubscriptionUI() on the editor path.
88function showSharedEditorPromptBadge() {
89    if (!isSharedSiteEditor()) return;
90    const usage = window.sessionData?.ownerPromptUsage;
91    if (!usage) return;
92    const headerActions = document.querySelector('.header-actions');
93    if (!headerActions) return;
94
95    const existing = headerActions.querySelector('.subscription-badge');
96    if (existing) existing.remove();
97
98    const limit = typeof usage.limit === 'number' ? usage.limit : 0;
99    const count = usage.count || 0;
100    const countText = limit < 0
101        ? 'Prompts: unlimited'
102        : voTn('subscription.badge.count', limit, { count, limit });
103
104    const badge = document.createElement('div');
105    badge.className = 'subscription-badge shared-editor-prompt-badge';
106    badge.style.cursor = 'default';
107    badge.innerHTML = `<span class="chat-count" data-prompts-copy>${countText}</span>`;
108    badge.title = "Prompts count against the site owner's plan";
109    headerActions.insertBefore(badge, headerActions.firstChild);
110}
111
112function syncSubscriptionStatusForSessionAccess() {
113    if (!isBuilderPage() || !window.sessionData) return;
114    if (isSharedSiteEditor()) {
115        clearSharedEditorSubscriptionUI();
116        showSharedEditorPromptBadge();
117        return;
118    }
119    if (firebase.auth().currentUser && !window.currentSubscription) {
120        checkSubscriptionStatus();
121    }
122}
123window.syncSubscriptionStatusForSessionAccess = syncSubscriptionStatusForSessionAccess;
124
125// Localized string lookup (catalogs in /js/i18n-catalog.*.js); falls back to
126// the key if the i18n runtime is missing
127function voT(key, params) {
128    return window.voI18n ? window.voI18n.t(key, params) : key;
129}
130function voTn(key, count, params) {
131    return window.voI18n ? window.voI18n.tn(key, count, params) : key;
132}
133function voDate(d) {
134    return window.voI18n ? window.voI18n.fmtDate(d) : new Date(d).toLocaleDateString();
135}
136
137function getRemainingChats(subscription) {
138    return Math.max(0, (subscription.monthly_chat_limit || 0) - (subscription.monthly_chat_count || 0));
139}
140
141function formatSubscriptionTierName(tier) {
142    if (tier === 'side_hustle') return 'SIDE HUSTLE';
143    return String(tier || 'free').toUpperCase();
144}
145
146function getFreeChatWarningKey() {
147    const user = firebase.auth().currentUser;
148    const userKey = user?.uid || 'anonymous';
149    const monthKey = new Date().toISOString().slice(0, 7);
150    return `vibeotter_free_chat_warning_${userKey}_${monthKey}`;
151}
152
153function hasSeenFreeChatWarning() {
154    try {
155        return localStorage.getItem(getFreeChatWarningKey()) === 'true';
156    } catch (error) {
157        return false;
158    }
159}
160
161function markFreeChatWarningSeen() {
162    try {
163        localStorage.setItem(getFreeChatWarningKey(), 'true');
164    } catch (error) {
165        // Ignore storage failures.
166    }
167}
168
169function closeFreeChatWarningPopover() {
170    const popover = document.getElementById('freeChatWarningPopover');
171    if (popover) {
172        popover.remove();
173    }
174}
175
176function startSideHustleFromFreeChatWarning() {
177    closeFreeChatWarningPopover();
178    if (typeof window.showPaywallModal === 'function') {
179        window.showPaywallModal({ title: voT('subscription.paywall_title') });
180    }
181}
182
183function maybeShowFreeChatWarning(subscription) {
184    if (!isBuilderPage()) return;
185    if (!subscription || subscription.tier !== 'free') return;
186
187    const remaining = getRemainingChats(subscription);
188    if (remaining > 2 || hasSeenFreeChatWarning()) return;
189
190    markFreeChatWarningSeen();
191    closeFreeChatWarningPopover();
192
193    const popover = document.createElement('div');
194    popover.id = 'freeChatWarningPopover';
195    popover.className = 'free-chat-warning-popover';
196    popover.setAttribute('role', 'dialog');
197    popover.setAttribute('aria-labelledby', 'freeChatWarningTitle');
198    popover.innerHTML = `
199        <button class="free-chat-warning-close" type="button" aria-label="${voT('subscription.popover.close_aria')}" onclick="closeFreeChatWarningPopover()">&times;</button>
200        <h2 id="freeChatWarningTitle" data-prompts-copy>${voTn('subscription.popover.remaining', remaining)}</h2>
201        <p data-prompts-copy>${voT('subscription.popover.upsell')}</p>
202        <div class="free-chat-warning-actions">
203            <button type="button" class="btn-secondary" onclick="closeFreeChatWarningPopover()">${voT('subscription.popover.keep_free')}</button>
204            <button type="button" class="btn-primary" onclick="startSideHustleFromFreeChatWarning()">${voT('subscription.popover.upgrade')}</button>
205        </div>
206    `;
207
208    document.body.appendChild(popover);
209}
210
211function updateSubscriptionUI(subscription) {
212    // Add subscription indicator to header if exists
213    const headerActions = document.querySelector('.header-actions');
214    if (headerActions) {
215        // Remove existing subscription badge if any
216        const existingBadge = headerActions.querySelector('.subscription-badge');
217        if (existingBadge) {
218            existingBadge.remove();
219        }
220        
221        if (subscription.tier === 'free') {
222            // Free plan: always show remaining Prompts so users know where they stand.
223            const remaining = getRemainingChats(subscription);
224            const badge = document.createElement('div');
225            badge.className = 'subscription-badge';
226            if (remaining <= 2) {
227                badge.classList.add('free-chat-low');
228            }
229            if (remaining <= 1) {
230                badge.classList.add('free-chat-critical');
231            }
232            badge.innerHTML = `<span class="chat-count" data-prompts-copy>${voTn('subscription.badge.count', subscription.monthly_chat_limit || 0, { count: subscription.monthly_chat_count || 0, limit: subscription.monthly_chat_limit || 0 })}</span>`;
233            badge.title = voT('subscription.badge.upgrade_title');
234            badge.addEventListener('click', () => {
235                if (typeof window.showPaywallModal === 'function') {
236                    window.showPaywallModal({ title: voT('subscription.paywall_title') });
237                }
238            });
239            headerActions.insertBefore(badge, headerActions.firstChild);
240        } else if (subscription.tier) {
241            const badge = document.createElement('div');
242            badge.className = 'subscription-badge';
243            
244            const monthlyPromptLimit = subscription.monthly_chat_limit || 0;
245            const promptDisplay = voTn('subscription.badge.count', monthlyPromptLimit, { count: subscription.monthly_chat_count || 0, limit: monthlyPromptLimit });
246            let carriedOverPromptDisplay = '';
247
248            // retainedCredits is the API contract; present it as carried-over Prompts.
249            if (subscription.retainedCredits) {
250                const expireDate = voDate(subscription.retainedCredits.expiresAt);
251                carriedOverPromptDisplay = `<span class="retained-prompt-count" data-prompts-copy>${voT('subscription.badge.carried', { tier: subscription.retainedCredits.fromTier.toUpperCase(), date: expireDate })}</span>`;
252            }
253            
254            // Check if we're on builder.html - if so, use simplified display
255            const isBuilderPage = window.location.pathname.includes('builder.html') || 
256                                 window.location.pathname.endsWith('builder') ||
257                                 document.querySelector('.builder-container');
258            
259            if (isBuilderPage) {
260                // Simplified display for builder page only - just show plan and Prompt count, no dates.
261                badge.innerHTML = `
262                    <span class="tier-name">${formatSubscriptionTierName(subscription.tier)}</span>
263                    <span class="chat-count" data-prompts-copy>${promptDisplay}</span>
264                `;
265            } else {
266                // Full display for other pages (dashboard, pricing, etc.)
267                // Handle scheduled downgrade
268                if (subscription.downgrade) {
269                    const downgradeDate = voDate(subscription.downgrade.effectiveDate);
270                    badge.innerHTML = `
271                        <span class="tier-name" style="color: #F59E0B;">${voT('subscription.badge.downgrades', { tier: formatSubscriptionTierName(subscription.tier), date: downgradeDate })}</span>
272                        <span class="chat-count" data-prompts-copy>${promptDisplay}</span>
273                        ${carriedOverPromptDisplay}
274                    `;
275                }
276                // Handle cancelled subscriptions
277                else if (subscription.status === 'cancel_at_period_end' || subscription.cancel_at_period_end) {
278                    const cancelDate = voDate(subscription.currentPeriodEnd);
279                    badge.innerHTML = `
280                        <span class="tier-name" style="color: #DC2626;">${voT('subscription.badge.cancels', { tier: formatSubscriptionTierName(subscription.tier), date: cancelDate })}</span>
281                        <span class="chat-count" data-prompts-copy>${promptDisplay}</span>
282                        ${carriedOverPromptDisplay}
283                    `;
284                } else {
285                    badge.innerHTML = `
286                        <span class="tier-name">${formatSubscriptionTierName(subscription.tier)}</span>
287                        <span class="chat-count" data-prompts-copy>${promptDisplay}</span>
288                        ${carriedOverPromptDisplay}
289                    `;
290                }
291            }
292            
293            badge.title = 'Manage site plans from your dashboard';
294
295            // Route users to the dashboard where plan changes are site-scoped.
296            badge.addEventListener('click', () => {
297                if (window.stripeIntegration) {
298                    window.stripeIntegration.openPortal();
299                }
300            });
301            
302            headerActions.insertBefore(badge, headerActions.firstChild);
303        }
304    }
305    
306    // Update dashboard account management if it exists
307    if (window.updateAccountManagementUI) {
308        window.updateAccountManagementUI(subscription);
309    }
310
311    // Update the customer-facing Prompt usage summary bar when the dashboard provides it.
312    if (window.updateChatUsageSummary) {
313        window.updateChatUsageSummary();
314    }
315}
316
317// Add styles for subscription badge
318const style = document.createElement('style');
319style.textContent = `
320    .subscription-badge {
321        display: flex;
322        align-items: center;
323        gap: 0.75rem;
324        padding: 0.5rem 1rem;
325        background: rgba(45, 125, 125, 0.1);
326        border: 1px solid var(--border-color);
327        border-radius: 0.5rem;
328        cursor: pointer;
329        transition: all 0.2s;
330        white-space: nowrap;
331        min-height: 36px;
332        box-sizing: border-box;
333    }
334    
335    .subscription-badge:hover {
336        border-color: var(--primary-color);
337        transform: translateY(-1px);
338    }
339
340    .subscription-badge.free-chat-low {
341        background: rgba(255, 111, 97, 0.12);
342        border-color: rgba(255, 111, 97, 0.55);
343    }
344
345    .subscription-badge.free-chat-low .chat-count {
346        color: #d45b50;
347        font-weight: 700;
348    }
349
350    .subscription-badge.free-chat-critical {
351        background: rgba(185, 54, 44, 0.14);
352        border-color: rgba(185, 54, 44, 0.65);
353    }
354
355    .subscription-badge.free-chat-critical .chat-count {
356        color: #b9362c;
357    }
358    
359    .tier-name {
360        font-weight: 600;
361        color: var(--primary-color);
362        font-size: 0.875rem;
363    }
364    
365    .chat-count {
366        font-size: 0.75rem;
367        color: var(--primary-color);
368    }
369
370    .subscription-badge .retained-prompt-count {
371        font-size: 0.7rem;
372        font-weight: 500;
373        color: #10B981;
374    }
375
376    .free-chat-warning-popover {
377        position: fixed;
378        top: 72px;
379        right: 16px;
380        z-index: 1300;
381        width: min(360px, calc(100vw - 32px));
382        padding: 1rem;
383        background: rgba(255, 252, 247, 0.96);
384        border: 2.5px solid rgba(45, 125, 125, 0.15);
385        border-radius: 8px;
386        box-shadow: 4px 4px 0px #4A9B9B, 0 18px 42px rgba(15, 23, 42, 0.18);
387        color: #3D3229;
388    }
389
390    .free-chat-warning-popover h2 {
391        margin: 0 1.75rem 0.5rem 0;
392        font-size: 1rem;
393        line-height: 1.25;
394        color: #3D3229;
395    }
396
397    .free-chat-warning-popover p {
398        margin: 0;
399        color: var(--text-secondary, #4b5563);
400        font-size: 0.9rem;
401        line-height: 1.45;
402    }
403
404    .free-chat-warning-actions {
405        display: flex;
406        justify-content: flex-end;
407        gap: 0.75rem;
408        margin-top: 1rem;
409    }
410
411    .free-chat-warning-close {
412        position: absolute;
413        top: 0.65rem;
414        right: 0.7rem;
415        width: 28px;
416        height: 28px;
417        border: 0;
418        background: transparent;
419        color: var(--text-secondary, #6b7280);
420        cursor: pointer;
421        font-size: 1.2rem;
422        line-height: 1;
423    }
424
425    @media (max-width: 520px) {
426        .free-chat-warning-popover {
427            top: 64px;
428            left: 16px;
429            right: 16px;
430            width: auto;
431        }
432
433        .free-chat-warning-actions {
434            flex-direction: column-reverse;
435        }
436
437        .free-chat-warning-actions .btn-primary,
438        .free-chat-warning-actions .btn-secondary {
439            width: 100%;
440        }
441    }
442    
443`;
444document.head.appendChild(style);
445
446// Check subscription on auth state change (debounced to prevent rapid-fire calls).
447// Cached subscription state belongs to one user: clear it whenever the signed-in
448// user changes or signs out, so the next account never reads the previous one's data.
449let subscriptionCheckTimeout = null;
450let lastSubscriptionAuthUid = null;
451firebase.auth().onAuthStateChanged((user) => {
452    if (user?.uid !== lastSubscriptionAuthUid) {
453        lastSubscriptionAuthUid = user?.uid || null;
454        window.siteSubscriptions = [];
455        window.currentSubscription = null;
456    }
457    if (user) {
458        if (subscriptionCheckTimeout) clearTimeout(subscriptionCheckTimeout);
459        subscriptionCheckTimeout = setTimeout(() => checkSubscriptionStatus(), 500);
460    }
461});
462
463// Polling function for subscription status (called on specific user interactions)
464function pollSubscriptionStatus() {
465    // Only poll if user is authenticated
466    const user = firebase.auth().currentUser;
467    if (user) {
468        console.log('Polling subscription status...');
469        checkSubscriptionStatus();
470    }
471}
472
473// Poll on specific interactions
474function setupSubscriptionPolling() {
475    // Poll when visiting pricing page
476    if (window.location.pathname.includes('pricing')) {
477        pollSubscriptionStatus();
478    }
479    
480    // Poll when clicking cancel button (before showing modal)
481    window.addEventListener('click', (e) => {
482        if (e.target && e.target.id === 'cancelPlanBtn') {
483            pollSubscriptionStatus();
484        }
485    });
486    
487    // Poll when showing any pricing-related modals
488    const observer = new MutationObserver((mutations) => {
489        mutations.forEach((mutation) => {
490            if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
491                const target = mutation.target;
492                if (target.classList.contains('show') && 
493                    (target.classList.contains('cancel-modal') || 
494                     target.classList.contains('pricing-modal'))) {
495                    pollSubscriptionStatus();
496                }
497            }
498        });
499    });
500    
501    // Observe modal changes
502    document.addEventListener('DOMContentLoaded', () => {
503        const modals = document.querySelectorAll('.cancel-modal, .pricing-modal, .paywall-modal');
504        modals.forEach(modal => {
505            observer.observe(modal, { attributes: true, attributeFilter: ['class'] });
506        });
507    });
508}
509
510// Export for use in other scripts
511window.checkSubscriptionStatus = checkSubscriptionStatus;
512window.pollSubscriptionStatus = pollSubscriptionStatus;
513window.closeFreeChatWarningPopover = closeFreeChatWarningPopover;
514window.startSideHustleFromFreeChatWarning = startSideHustleFromFreeChatWarning;
515
516// Setup polling when script loads
517setupSubscriptionPolling();
518
519// Additional polling triggers
520document.addEventListener('DOMContentLoaded', () => {
521    // Poll when account management button is clicked
522    const accountBtn = document.querySelector('.account-management-btn');
523    if (accountBtn) {
524        accountBtn.addEventListener('click', pollSubscriptionStatus);
525    }
526    // Note: Removed expensive MutationObserver on document.body that was causing Chrome crashes
527    // The modal-specific observer in setupSubscriptionPolling() handles paywall detection
528});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.