UNPKG

@mobiloud/push-banner-widget

Version:

Smart, responsive banner widget for enabling push notifications in MobiLoud apps. Features session limiting, mobile optimization, and zero dependencies.

810 lines (683 loc) 23.8 kB
/** * Push Notification Banner Widget * A smart banner that prompts users to enable push notifications * Integrated with MobiLoud app platform * * Usage: * const banner = createPushBanner({ * heading: "Get 10% OFF your next order", * text: "Enable push notifications to receive your unique coupon code", * position: 'top', * sessionLimit: 3, * debugMode: false * }); */ // Inject CSS styles for the banner widget (function injectBannerStyles() { // Check if styles are already injected if (document.getElementById('push-banner-widget-styles')) return; const styles = ` /* Push Notification Banner Widget Styles */ .push-banner { width: 100%; box-sizing: border-box; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 9999; /* Default styling */ background-color: #e3f2fd; color: #475569; /* Prevent text selection */ -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } /* Banner content container */ .push-banner-content { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; min-height: 60px; max-width: 1200px; margin: 0 auto; position: relative; transition: all 0.3s ease; } /* Text content wrapper */ .push-banner-text { flex: 1; min-width: 0; /* Allows text to shrink */ margin-right: 16px; transition: all 0.3s ease; } /* Banner heading */ .push-banner-heading { font-size: 15px; font-weight: 600; margin: 0 0 4px 0; line-height: 1.3; transition: all 0.3s ease; } /* Banner description text */ .push-banner-description { font-size: 14px; font-weight: 400; margin: 0; line-height: 1.4; opacity: 0.9; transition: all 0.3s ease; } /* Success message styling */ .push-banner-success { font-size: 15px; font-weight: 500; margin: 0; line-height: 1.4; transition: all 0.3s ease; } /* Chevron icon */ .push-banner-icon { flex-shrink: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; opacity: 0.8; transition: all 0.3s ease; } /* CSS Chevron */ .push-banner-chevron { width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg); transition: transform 0.3s ease; } /* Icon animation on hover */ .push-banner:hover .push-banner-icon { opacity: 1; transform: translateX(2px); } /* Active state */ .push-banner:active { transform: scale(0.995); } /* Position: Fixed Top */ .push-banner.position-top { position: fixed; top: 0; left: 0; right: 0; transform: translateY(-100%); } .push-banner.position-top.visible { transform: translateY(0); } /* Position: Fixed Bottom */ .push-banner.position-bottom { position: fixed; bottom: 0; left: 0; right: 0; transform: translateY(100%); } .push-banner.position-bottom.visible { transform: translateY(0); } /* Position: Within wrapper element */ .push-banner.position-wrapper { position: relative; opacity: 0; transform: translateY(-10px); } .push-banner.position-wrapper.visible { opacity: 1; transform: translateY(0); } /* Success state modifications */ .push-banner.success-state .push-banner-heading { opacity: 0; height: 0; margin: 0; overflow: hidden; transform: translateY(-10px); } .push-banner.success-state .push-banner-description { opacity: 0; height: 0; margin: 0; overflow: hidden; transform: translateY(-10px); } .push-banner.success-state .push-banner-icon { opacity: 0; transform: translateX(20px); } .push-banner.success-state .push-banner-success { opacity: 1; height: auto; transform: translateY(0); } /* Hide success message initially */ .push-banner-success { opacity: 0; height: 0; overflow: hidden; transform: translateY(10px); } /* Hiding animation */ .push-banner.hiding { opacity: 0; } .push-banner.hiding.position-top { transform: translateY(-100%); } .push-banner.hiding.position-bottom { transform: translateY(100%); } .push-banner.hiding.position-wrapper { transform: translateY(-10px); } /* Tablet styles */ @media (max-width: 768px) { .push-banner-content { padding: 14px 16px; min-height: 56px; } .push-banner-heading { font-size: 15px; margin-bottom: 2px; } .push-banner-description { font-size: 14px; } .push-banner-success { font-size: 15px; } .push-banner-text { margin-right: 12px; } .push-banner-icon { width: 20px; height: 20px; } .push-banner-chevron { width: 7px; height: 7px; } } /* Mobile styles */ @media (max-width: 480px) { .push-banner-content { padding: 12px 16px; min-height: 52px; } .push-banner-heading { font-size: 15px; margin-bottom: 2px; } .push-banner-description { font-size: 14px; } .push-banner-success { font-size: 15px; } .push-banner-text { margin-right: 10px; } .push-banner-icon { width: 18px; height: 18px; } .push-banner-chevron { width: 6px; height: 6px; } } /* Small mobile styles */ @media (max-width: 320px) { .push-banner-content { padding: 10px 12px; } .push-banner-heading { font-size: 14px; } .push-banner-description { font-size: 13px; } .push-banner-success { font-size: 14px; } .push-banner-text { margin-right: 8px; } .push-banner-chevron { width: 5px; height: 5px; } } /* Accessibility */ @media (prefers-reduced-motion: reduce) { .push-banner, .push-banner *, .push-banner:hover .push-banner-icon { transition: none; transform: none; } .push-banner:hover .push-banner-icon { transform: none; } } /* Focus styles for accessibility */ .push-banner:focus { outline: 2px solid rgba(255, 255, 255, 0.5); outline-offset: -2px; } /* Dark mode support */ @media (prefers-color-scheme: dark) { .push-banner { background-color: #1a202c; color: #e2e8f0; } } /* High contrast mode support */ @media (prefers-contrast: high) { .push-banner { border: 2px solid currentColor; } .push-banner-icon { opacity: 1; } .push-banner-description { opacity: 1; } } `; const styleSheet = document.createElement('style'); styleSheet.id = 'push-banner-widget-styles'; styleSheet.textContent = styles; document.head.appendChild(styleSheet); })(); class PushNotificationBanner { constructor(options = {}) { this.options = { heading: options.heading || "Get 10% OFF your next order", text: options.text || "Enable push notifications to receive your unique coupon code", successMessage: options.successMessage || "Thank you for subscribing! Use APPLOVER coupon to get your 10% discount", position: options.position || 'top', // 'top', 'bottom', or { element: '#selector' } displayMode: options.displayMode || 'fixed', // 'fixed', 'relative', 'scroll' sessionLimit: options.sessionLimit || 3, backgroundColor: options.backgroundColor || '#e3f2fd', headingColor: options.headingColor || '#1e293b', textColor: options.textColor || '#475569', autoHideSuccess: options.autoHideSuccess !== false, // Default true debugMode: options.debugMode || false, onAccept: options.onAccept || (() => {}), onShow: options.onShow || (() => {}), onHide: options.onHide || (() => {}), allowedUrls: options.allowedUrls || null }; this.isVisible = false; this.banner = null; this.currentPushStatus = null; this.statusCheckInterval = null; this.sessionKey = 'pushBanner_' + this.generateSessionKey(); this._scrollHandler = null; this.init(); } generateSessionKey() { // Generate a unique key based on position and content const position = typeof this.options.position === 'object' ? this.options.position.element : this.options.position; // Create a simple hash from the combined string const combined = position + this.options.heading + this.options.text; let hash = 0; for (let i = 0; i < combined.length; i++) { const char = combined.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } // Convert to positive number and then to alphanumeric string return Math.abs(hash).toString(36).slice(0, 10); } init() { // Check if current URL is allowed if (!this.isUrlAllowed()) { return; } // Check session limit if (!this.checkSessionLimit()) { return; } // Check if should show banner if (!this.shouldShowBanner()) { return; } this.createBanner(); this.setupPushStatusMonitoring(); this.show(); } isInApp() { // Check if user agent contains "canvas" (case insensitive) return navigator.userAgent.toLowerCase().includes('canvas'); } isPushEnabled() { try { // Check MobiLoud push notification status return !!(window.mobiloudAppInfo && window.mobiloudAppInfo.pushSubscribed); } catch (e) { // If can't determine status, assume disabled return false; } } shouldShowBanner() { // In debug mode, bypass all checks if (this.options.debugMode) { console.log('[PushBanner Debug] Debug mode enabled - bypassing app and push checks'); return true; } // Check if user is in the app if (!this.isInApp()) { console.log('[PushBanner] Not in app context - banner will not show'); return false; } // Check if push notifications are already enabled if (this.isPushEnabled()) { console.log('[PushBanner] Push notifications already enabled - banner will not show'); return false; } return true; } checkSessionLimit() { try { const sessionCount = parseInt(sessionStorage.getItem(this.sessionKey) || '0', 10); if (sessionCount >= this.options.sessionLimit) { console.log(`[PushBanner] Session limit reached (${sessionCount}/${this.options.sessionLimit})`); return false; } return true; } catch (e) { console.error('[PushBanner] Error checking session limit:', e); return true; // Fallback to showing banner } } incrementSessionCount() { try { const currentCount = parseInt(sessionStorage.getItem(this.sessionKey) || '0', 10); sessionStorage.setItem(this.sessionKey, (currentCount + 1).toString()); } catch (e) { console.error('[PushBanner] Error incrementing session count:', e); } } setupPushStatusMonitoring() { // Set up real-time push status monitoring this.currentPushStatus = this.isPushEnabled(); // Override the global push status change callback const originalCallback = window.mlPushStatusChanged; window.mlPushStatusChanged = (isSubscribed) => { // Call original callback if it existed if (typeof originalCallback === 'function') { originalCallback(isSubscribed); } // Update our status and UI this.currentPushStatus = isSubscribed; this.updateUIForPushStatus(isSubscribed); }; // Poll for status changes as backup this.statusCheckInterval = setInterval(() => { const newStatus = this.isPushEnabled(); if (newStatus !== this.currentPushStatus) { this.currentPushStatus = newStatus; this.updateUIForPushStatus(newStatus); } }, 1000); } updateUIForPushStatus(isEnabled) { if (!this.banner || !isEnabled) return; // Show success message this.showSuccessMessage(); // Auto-hide after 3 seconds if enabled if (this.options.autoHideSuccess) { setTimeout(() => { this.hide(); }, 3000); } } showSuccessMessage() { if (!this.banner) return; this.banner.classList.add('success-state'); // Update the success message text const successElement = this.banner.querySelector('.push-banner-success'); if (successElement) { successElement.textContent = this.options.successMessage; } } isUrlAllowed() { // If no URL restrictions specified, allow all URLs if (!this.options.allowedUrls) { return true; } const currentUrl = window.location.href; const currentPath = window.location.pathname; const allowedUrls = Array.isArray(this.options.allowedUrls) ? this.options.allowedUrls : [this.options.allowedUrls]; return allowedUrls.some(pattern => { // Exact match if (pattern === currentUrl || pattern === currentPath) { return true; } // Wildcard matching if (pattern.includes('*')) { const regexPattern = pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') .replace(/\*/g, '.*'); const regex = new RegExp(`^${regexPattern}$`, 'i'); return regex.test(currentUrl) || regex.test(currentPath); } // Regex pattern if (pattern.startsWith('/') && pattern.endsWith('/')) { const regexPattern = pattern.slice(1, -1); const regex = new RegExp(regexPattern, 'i'); return regex.test(currentUrl) || regex.test(currentPath); } // Partial match return currentUrl.includes(pattern) || currentPath.includes(pattern); }); } createBanner() { // Create banner container this.banner = document.createElement('div'); this.banner.className = 'push-banner'; // Add position/displayMode class if (this.options.displayMode === 'relative') { this.banner.classList.add('position-relative'); } else if (this.options.displayMode === 'scroll') { this.banner.classList.add('position-top'); // Use fixed top, but control visibility with scroll this.banner.classList.add('scroll-activated'); } else if (typeof this.options.position === 'string') { this.banner.classList.add(`position-${this.options.position}`); } else { this.banner.classList.add('position-wrapper'); } // Apply custom colors this.banner.style.backgroundColor = this.options.backgroundColor; // Create content container const content = document.createElement('div'); content.className = 'push-banner-content'; // Create text container const textContainer = document.createElement('div'); textContainer.className = 'push-banner-text'; // Create heading const heading = document.createElement('div'); heading.className = 'push-banner-heading'; heading.textContent = this.options.heading; heading.style.color = this.options.headingColor; // Create description const description = document.createElement('div'); description.className = 'push-banner-description'; description.textContent = this.options.text; description.style.color = this.options.textColor; // Create success message (hidden initially) const successMessage = document.createElement('div'); successMessage.className = 'push-banner-success'; successMessage.textContent = this.options.successMessage; successMessage.style.color = this.options.headingColor; // Create icon const icon = document.createElement('div'); icon.className = 'push-banner-icon'; icon.innerHTML = '<div class="push-banner-chevron"></div>'; icon.style.color = this.options.textColor; // Assemble the banner textContainer.appendChild(heading); textContainer.appendChild(description); textContainer.appendChild(successMessage); content.appendChild(textContainer); content.appendChild(icon); this.banner.appendChild(content); // Add click handler this.banner.addEventListener('click', () => { this.triggerPushPrompt(); }); // Add keyboard support this.banner.setAttribute('tabindex', '0'); this.banner.setAttribute('role', 'button'); this.banner.setAttribute('aria-label', `${this.options.heading}. ${this.options.text}`); this.banner.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.triggerPushPrompt(); } }); // Position the banner this.positionBanner(); } positionBanner() { if (this.options.displayMode === 'relative') { // Insert as first child of body (normal flow) document.body.insertBefore(this.banner, document.body.firstChild); } else if (this.options.displayMode === 'scroll') { // Fixed at top, but only visible after scroll document.body.appendChild(this.banner); this.banner.classList.remove('visible'); // Hide initially this._scrollHandler = () => { if (window.scrollY > 0) { if (!this.isVisible) { this.show(); } } else { if (this.isVisible) { this.hide(); } } }; window.addEventListener('scroll', this._scrollHandler); // Initial check this._scrollHandler(); } else if (typeof this.options.position === 'string') { // Fixed positioning (default) document.body.appendChild(this.banner); } else { // Position within specified element const targetElement = document.querySelector(this.options.position.element); if (targetElement) { targetElement.appendChild(this.banner); } else { console.error(`[PushBanner] Target element "${this.options.position.element}" not found`); // Fallback to body document.body.appendChild(this.banner); } } } triggerPushPrompt() { try { // Call user's onAccept callback first this.options.onAccept(); // Check if we're in debug mode if (this.options.debugMode) { console.log('[PushBanner Debug] Debug mode - simulating push prompt acceptance'); setTimeout(() => { this.updateUIForPushStatus(true); }, 1000); return; } // Safety checks for native functions if (typeof nativeFunctions === 'undefined') { throw new Error('nativeFunctions not available'); } if (typeof nativeFunctions.triggerPushPrompt !== 'function') { throw new Error('triggerPushPrompt not available'); } // Call the native function nativeFunctions.triggerPushPrompt(); } catch (e) { console.error('[PushBanner] Error triggering push prompt:', e); } } show() { if (this.isVisible || !this.banner) return; this.isVisible = true; // Increment session count this.incrementSessionCount(); // Show banner with animation requestAnimationFrame(() => { this.banner.classList.add('visible'); document.body.classList.add('push-banner-visible'); }); // Call onShow callback this.options.onShow(); } hide() { if (!this.isVisible || !this.banner) return; this.isVisible = false; // Hide banner with animation this.banner.classList.add('hiding'); document.body.classList.remove('push-banner-visible'); setTimeout(() => { if (this.banner && this.banner.parentNode) { this.banner.parentNode.removeChild(this.banner); } // For scroll mode, re-insert the banner so it can show again after scroll up if (this.options.displayMode === 'scroll') { document.body.appendChild(this.banner); this.banner.classList.remove('hiding', 'visible'); } }, 300); // Call onHide callback this.options.onHide(); } destroy() { // Clean up status monitoring if (this.statusCheckInterval) { clearInterval(this.statusCheckInterval); } // Remove scroll event if needed if (this._scrollHandler) { window.removeEventListener('scroll', this._scrollHandler); this._scrollHandler = null; } // Remove banner if (this.banner && this.banner.parentNode) { this.banner.parentNode.removeChild(this.banner); } document.body.classList.remove('push-banner-visible'); this.isVisible = false; this.banner = null; } } // Global function for easy usage window.createPushBanner = function(options) { return new PushNotificationBanner(options); }; // Auto-initialize if window.pushBannerConfig exists if (typeof window.pushBannerConfig !== 'undefined') { window.pushBanner = new PushNotificationBanner(window.pushBannerConfig); }