// pushFlicIntegration.js import { PUBLIC_VAPID_KEY, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS } from './config.js'; let pushSubscription = null; // Keep track locally if needed let actionHandlers = {}; // Store handlers for different Flic actions // --- Helper Functions --- // Get stored basic auth credentials or prompt user for them function getBasicAuthCredentials() { const storedAuth = localStorage.getItem('basicAuthCredentials'); if (storedAuth) { try { const credentials = JSON.parse(storedAuth); // Check if the credentials are valid if (credentials.username && credentials.password) { console.log('Using stored basic auth credentials.'); return credentials; } } catch (error) { console.error('Failed to parse stored credentials:', error); } } // No valid stored credentials found // The function will return null and the caller should handle prompting if needed console.log('No valid stored credentials found.'); return null; } // Prompt the user for credentials after permissions are granted function promptForCredentials() { console.log('Prompting user for auth credentials.'); const username = prompt('Please enter your username for backend authentication:'); if (!username) return null; const password = prompt('Please enter your password:'); if (!password) return null; const credentials = { username, password }; localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials)); return credentials; } // Create Basic Auth header string function createBasicAuthHeader(credentials) { if (!credentials?.username || !credentials.password) return null; return 'Basic ' + btoa(`${credentials.username}:${credentials.password}`); } // Convert URL-safe base64 string to Uint8Array function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // Convert ArrayBuffer to URL-safe Base64 string function arrayBufferToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // --- Push Subscription Logic --- async function subscribeToPush() { const buttonId = FLIC_BUTTON_ID; // Use configured button ID if (!('serviceWorker' in navigator) || !('PushManager' in window)) { console.error('Push Messaging is not supported.'); alert('Push Notifications are not supported by your browser.'); return; } try { // First request notification permission console.log('Requesting notification permission...'); const permission = await Notification.requestPermission(); if (permission !== 'granted') { console.warn('Notification permission denied.'); alert('Please enable notifications to link the Flic button.'); return; } console.log('Notification permission granted.'); // After permission is granted, check for stored credentials or prompt user let credentials = getBasicAuthCredentials(); if (!credentials) { const confirmAuth = confirm('Do you want to set up credentials for push notifications now?'); if (!confirmAuth) { console.log('User declined to provide auth credentials.'); return; } credentials = promptForCredentials(); if (!credentials) { console.log('User canceled credential input.'); alert('Authentication required to set up push notifications.'); return; } } const registration = await navigator.serviceWorker.ready; let existingSubscription = await registration.pushManager.getSubscription(); let needsResubscribe = !existingSubscription; if (existingSubscription) { const existingKey = existingSubscription.options?.applicationServerKey; if (!existingKey || arrayBufferToBase64(existingKey) !== PUBLIC_VAPID_KEY) { console.log('VAPID key mismatch or missing. Unsubscribing old subscription.'); await existingSubscription.unsubscribe(); existingSubscription = null; needsResubscribe = true; } else { console.log('Existing valid subscription found.'); pushSubscription = existingSubscription; // Store it } } let finalSubscription = existingSubscription; if (needsResubscribe) { console.log('Subscribing for push notifications...'); const applicationServerKey = urlBase64ToUint8Array(PUBLIC_VAPID_KEY); finalSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }); console.log('New push subscription obtained:', finalSubscription); pushSubscription = finalSubscription; // Store it } if (!finalSubscription) { console.error("Failed to obtain a subscription object."); alert("Could not get subscription details."); return; } await sendSubscriptionToServer(finalSubscription, buttonId); } catch (error) { console.error('Error during push subscription:', error); alert(`Subscription failed: ${error.message}`); } } async function sendSubscriptionToServer(subscription, buttonId) { console.log(`Sending subscription for button "${buttonId}" to backend...`); const credentials = getBasicAuthCredentials(); if (!credentials) { // One more chance to enter credentials if needed const confirmAuth = confirm('Authentication required to complete setup. Provide credentials now?'); if (!confirmAuth) { alert('Authentication required to save button link.'); return; } const newCredentials = promptForCredentials(); if (!newCredentials) { alert('Authentication required to save button link.'); return; } credentials = newCredentials; } const headers = { 'Content-Type': 'application/json' }; const authHeader = createBasicAuthHeader(credentials); if (authHeader) headers['Authorization'] = authHeader; try { // Add support for handling CORS preflight with credentials const response = await fetch(`${BACKEND_URL}/subscribe`, { method: 'POST', body: JSON.stringify({ button_id: buttonId, subscription: subscription }), headers: headers, credentials: 'include' // This ensures credentials are sent with OPTIONS requests too }); if (response.ok) { const result = await response.json(); console.log('Subscription sent successfully:', result.message); alert('Push notification setup completed successfully!'); } else { let errorMsg = `Server error: ${response.status}`; if (response.status === 401 || response.status === 403) { localStorage.removeItem('basicAuthCredentials'); // Clear bad creds errorMsg = 'Authentication failed. Please try again.'; } else { try { errorMsg = (await response.json()).message || errorMsg; } catch (e) { /* use default */ } } console.error('Failed to send subscription:', errorMsg); alert(`Failed to save link: ${errorMsg}`); } } catch (error) { console.error('Network error sending subscription:', error); alert(`Network error: ${error.message}`); } } // --- Flic Action Handling --- // Called by app.js when a message is received from the service worker export function handleFlicAction(action, buttonId, timestamp) { console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} at ${timestamp}`); // Ignore actions from buttons other than the configured one if (buttonId !== FLIC_BUTTON_ID) { console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`); return; } // Find the registered handler for this action const handler = actionHandlers[action]; if (handler && typeof handler === 'function') { console.log(`[PushFlic] Executing handler for ${action}`); // Execute the handler registered in app.js handler(); // Use the handler function directly instead of hardcoded function calls } else { console.warn(`[PushFlic] No handler registered for action: ${action}`); } } // --- Initialization --- export function initPushFlic(handlers) { actionHandlers = handlers; // Store the handlers passed from app.js // Example: handlers = { SingleClick: handleNextPlayer, Hold: handleTogglePause } // Attempt to subscribe immediately if permission might already be granted // Or trigger subscription on a user action (e.g., a "Link Flic Button" button) // For simplicity, let's try subscribing if SW is ready and permission allows if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { Notification.requestPermission().then(permission => { if (permission === 'granted') { console.log('[PushFlic] Permission granted, attempting subscription.'); subscribeToPush(); } else { console.log('[PushFlic] Notification permission not granted.'); // Optionally provide a button for the user to trigger subscription later } }); }); } }