// pushFlicIntegration.js import { getPublicVapidKey, 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.'); // Get stored credentials but don't prompt let credentials = getBasicAuthCredentials(); const hasExistingCreds = !!credentials; console.log('Has existing credentials:', hasExistingCreds); // No prompting for credentials - user must enter them manually in the UI if (!credentials) { console.log('No credentials found. User needs to enter them manually.'); // Just return if no credentials are available return; } const registration = await navigator.serviceWorker.ready; let existingSubscription = await registration.pushManager.getSubscription(); let needsResubscribe = !existingSubscription; console.log('Existing subscription found:', !!existingSubscription); if (existingSubscription) { const existingKey = existingSubscription.options?.applicationServerKey; if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) { 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(getPublicVapidKey()); try { finalSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }); console.log('New push subscription obtained:', finalSubscription); pushSubscription = finalSubscription; // Store it } catch (subscribeError) { console.error('Error subscribing to push:', subscribeError); alert(`Failed to subscribe: ${subscribeError.message}`); return; } } 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) { console.log('No credentials found. User needs to enter them manually.'); return; } 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); // Success alert removed as requested } 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, batteryLevel) { console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% 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}`); try { // Execute the handler registered in app.js handler(); // Log success console.log(`[PushFlic] Successfully executed handler for ${action}`); } catch (error) { console.error(`[PushFlic] Error executing handler for ${action}:`, error); } } else { console.warn(`[PushFlic] No handler registered for action: ${action}. Available handlers:`, Object.keys(actionHandlers)); } } // --- Initialization --- export function initPushFlic(handlers) { // Store the handlers passed from app.js if (handlers && Object.keys(handlers).length > 0) { actionHandlers = handlers; console.log('[PushFlic] Registered action handlers:', Object.keys(actionHandlers)); } else { console.warn('[PushFlic] No action handlers provided to initPushFlic, actions will not work!'); } // No longer auto-subscribe when permission is granted if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { Notification.requestPermission().then(permission => { if (permission === 'granted') { console.log('[PushFlic] Permission granted, but not automatically subscribing.'); console.log('[PushFlic] User can subscribe through "Push notification settings" menu.'); // subscribeToPush(); // Removed automatic subscription } else { console.log('[PushFlic] Notification permission not granted.'); } }); }); } } // New function to manually trigger the subscription process export function setupPushNotifications() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { console.log('[PushFlic] Manually triggering push subscription process...'); subscribeToPush(); }); } else { console.error('[PushFlic] Service workers not supported, cannot subscribe'); alert('Your browser does not support push notifications.'); } } // Function to force re-authentication even if credentials exist export function forceCredentialsPrompt() { // Get credentials from the form fields if available const usernameField = document.getElementById('pushUsername'); const passwordField = document.getElementById('pushPassword'); let credentialsUpdated = false; if (usernameField && passwordField && usernameField.value.trim() && passwordField.value.trim()) { // Save the entered credentials to localStorage const credentials = { username: usernameField.value.trim(), password: passwordField.value.trim() }; localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials)); console.log('[PushFlic] Saved credentials from form fields'); credentialsUpdated = true; } else { // Check if we have stored credentials try { const storedAuth = localStorage.getItem('basicAuthCredentials'); if (storedAuth) { const credentials = JSON.parse(storedAuth); if (credentials.username && credentials.password) { // We have stored credentials, no need to update console.log('[PushFlic] Using stored credentials'); credentialsUpdated = true; // Update the form fields if they exist if (usernameField && passwordField) { usernameField.value = credentials.username; passwordField.value = credentials.password; } } } } catch (error) { console.error('[PushFlic] Error accessing stored credentials:', error); } } if (!credentialsUpdated) { console.log('[PushFlic] No valid credentials available'); } // Trigger the subscription process setupPushNotifications(); }