1/** 2 * Enhanced SCORE Push Notifications with Anonymous User Support 3 * Version 2.0 4 */ 5 6class SCOREPushNotifications { 7 constructor() { 8 this.config = this.getConfig(); 9 this.vapidPublicKey = 'BIHx4pRAwO2v8HT6tX1G1CcPwWPTU_gtMHiJd1HmQfSShLEdrSDH3jAzSTDluMuYIGdShMTw0ndsSpUb99g4Fic'; 10 this.registration = null; 11 this.subscription = null; 12 this.anonymousUserId = this.getAnonymousUserId(); 13 this.userId = null; 14 } 15 16 /** 17 * Get or generate anonymous user ID 18 */ 19 getAnonymousUserId() { 20 let anonymousId = localStorage.getItem('score_anonymous_id'); 21 if (!anonymousId) { 22 anonymousId = 'anon_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); 23 localStorage.setItem('score_anonymous_id', anonymousId); 24 } 25 return anonymousId; 26 } 27 28 /** 29 * Get configuration based on hostname 30 */ 31 getConfig() { 32 const hostname = window.location.hostname; 33 const configs = { 34 'score-international.com': { 35 hostname: 'score-international.com', 36 API_BASE_URL: '/notify-api/api.php', 37 useProxy: true 38 }, 39 'score-development.com': { 40 hostname: 'score-development.com', 41 API_BASE_URL: '/notify-api/api.php', 42 useProxy: true 43 }, 44 'localhost': { 45 hostname: 'localhost', 46 API_BASE_URL: 'http://54.188.219.133:8000', 47 useProxy: false 48 } 49 }; 50 51 const config = configs[hostname] || configs['score-international.com']; 52 console.log('Push Notifications Config:', config); 53 console.log('Anonymous User ID:', this.anonymousUserId); 54 return config; 55 } 56 57 /** 58 * Make API request with anonymous user header 59 */ 60 async apiRequest(endpoint, options = {}) { 61 const url = this.config.useProxy 62 ? `${this.config.API_BASE_URL}?path=${encodeURIComponent(endpoint)}` 63 : `${this.config.API_BASE_URL}${endpoint}`; 64 65 const headers = { 66 'Content-Type': 'application/json', 67 'X-Anonymous-User-ID': this.anonymousUserId, 68 ...options.headers 69 }; 70 71 const response = await fetch(url, { 72 ...options, 73 headers 74 }); 75 76 if (!response.ok) { 77 const error = await response.text(); 78 throw new Error(error); 79 } 80 81 return response.json(); 82 } 83 84 /** 85 * Initialize push notifications 86 */ 87 async init() { 88 console.log('Initializing SCORE Push Notifications...'); 89 console.log('Using anonymous user ID:', this.anonymousUserId); 90 91 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { 92 console.error('Push notifications not supported'); 93 return false; 94 } 95 96 try { 97 // Register service worker 98 this.registration = await navigator.serviceWorker.register('/sw.js'); 99 console.log('Service Worker registered:', this.registration); 100 101 // Check current subscription 102 await this.checkSubscription(); 103 104 return true; 105 } catch (error) { 106 console.error('Failed to initialize push notifications:', error); 107 return false; 108 } 109 } 110 111 /** 112 * Check current subscription status 113 */ 114 async checkSubscription() { 115 try { 116 this.subscription = await this.registration.pushManager.getSubscription(); 117 118 if (this.subscription) { 119 console.log('Existing subscription found'); 120 // Verify with server 121 try { 122 const response = await this.apiRequest('/api/push/verify', { 123 method: 'POST', 124 body: JSON.stringify({ 125 endpoint: this.subscription.endpoint, 126 anonymous_id: this.anonymousUserId 127 }) 128 }); 129 130 if (response.userId) { 131 this.userId = response.userId; 132 console.log('Subscription verified for user:', this.userId); 133 } 134 } catch (error) { 135 console.log('Subscription not found on server'); 136 } 137 } else { 138 console.log('No existing subscription'); 139 } 140 141 this.updateUI(); 142 } catch (error) { 143 console.error('Error checking subscription:', error); 144 } 145 } 146 147 /** 148 * Toggle push subscription 149 */ 150 async toggleSubscription() { 151 if (this.subscription) { 152 await this.unsubscribeUser(); 153 } else { 154 await this.subscribeUser(); 155 } 156 } 157 158 /** 159 * Subscribe user to push notifications 160 */ 161 async subscribeUser() { 162 try { 163 // Request notification permission 164 const permission = await Notification.requestPermission(); 165 if (permission !== 'granted') { 166 throw new Error('Notification permission denied'); 167 } 168 169 // Create subscription 170 const subscription = await this.registration.pushManager.subscribe({ 171 userVisibleOnly: true, 172 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 173 }); 174 175 console.log('Push subscription created:', subscription); 176
177 // Send to server 178 console.log('Saving subscription for anonymous user:', this.anonymousUserId); 179 const response = await this.apiRequest('/api/push/subscribe', { 180 method: 'POST', 181 body: JSON.stringify({ 182 subscription: subscription.toJSON(), 183 anonymous_id: this.anonymousUserId 184 }) 185 }); 186 187 console.log('Server response:', response); 188 189 if (response.userId) { 190 this.userId = response.userId; 191 this.subscription = subscription; 192 console.log('Successfully subscribed! User ID:', this.userId); 193 this.updateUI(); 194 } else { 195 throw new Error('Failed to save subscription on server'); 196 } 197 } catch (error) { 198 console.error('Failed to subscribe:', error); 199 this.updateUI(); 200 throw error; 201 } 202 } 203 204 /** 205 * Unsubscribe user from push notifications 206 */ 207 async unsubscribeUser() { 208 try { 209 if (!this.subscription) return; 210 211 // Unsubscribe from push manager 212 await this.subscription.unsubscribe(); 213 214 // Notify server 215 await this.apiRequest('/api/push/unsubscribe', { 216 method: 'POST', 217 body: JSON.stringify({ 218 endpoint: this.subscription.endpoint, 219 anonymous_id: this.anonymousUserId, 220 user_id: this.userId 221 }) 222 }); 223 224 this.subscription = null; 225 this.userId = null; 226 console.log('Successfully unsubscribed'); 227 this.updateUI(); 228 } catch (error) { 229 console.error('Failed to unsubscribe:', error); 230 this.updateUI(); 231 } 232 } 233 234 /** 235 * Update UI based on subscription status 236 */ 237 updateUI() { 238 // Support both old and new element IDs for compatibility 239 const button = document.getElementById('push-toggle-btn') || 240 document.getElementById('push-notification-btn'); 241 const status = document.getElementById('push-status') || 242 document.getElementById('push-notification-status'); 243 244 if (button) { 245 if (this.subscription) { 246 button.textContent = 'Disable Push Notifications'; 247 button.classList.remove('btn-primary'); 248 button.classList.add('btn-secondary'); 249 } else { 250 button.textContent = 'Enable Push Notifications'; 251 button.classList.remove('btn-secondary'); 252 button.classList.add('btn-primary'); 253 } 254 } 255 256 if (status) { 257 if (this.subscription) { 258 status.textContent = `Push notifications enabled (User: ${this.userId || 'Anonymous'})`; 259 status.className = 'alert alert-success'; 260 } else { 261 status.textContent = 'Push notifications disabled'; 262 status.className = 'alert alert-info'; 263 } 264 } 265 } 266 267 /** 268 * Convert VAPID key 269 */ 270 urlBase64ToUint8Array(base64String) { 271 const padding = '='.repeat((4 - base64String.length % 4) % 4); 272 const base64 = (base64String + padding) 273 .replace(/\-/g, '+') 274 .replace(/_/g, '/'); 275 276 const rawData = window.atob(base64); 277 const outputArray = new Uint8Array(rawData.length); 278 279 for (let i = 0; i < rawData.length; ++i) { 280 outputArray[i] = rawData.charCodeAt(i); 281 } 282 return outputArray; 283 } 284} 285 286// Initialize on page load 287let scorePushNotifications; 288 289document.addEventListener('DOMContentLoaded', async () => { 290 scorePushNotifications = new SCOREPushNotifications(); 291 await scorePushNotifications.init(); 292}); 293 294// Global function for button onclick 295async function togglePushNotifications() { 296 if (!scorePushNotifications) return; 297 298 try { 299 await scorePushNotifications.toggleSubscription(); 300 } catch (error) { 301 alert('Failed to toggle push notifications: ' + error.message); 302 } 303}
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.