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()">×</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.