const serialResultsDiv = document.getElementById("serialResults"); const readBar = document.getElementById("readBar"); // Buffer for accumulating received data before logging let receivedDataBuffer = ''; // Software tool specific variables let deviceSerialNumber = ""; let deviceVersion = ""; let deviceHwVersion = ""; let selectedSoftwareUrl = ""; // Restricted hardware versions that require warning const RESTRICTED_HW_VERSIONS = ['r06', 'r06a', 'r07', 'r07a', 'r07b']; // Helper function to normalize hardware version for comparison function normalizeHwVersion(hwVersion) { if (!hwVersion) return ''; // Convert to lowercase and trim let normalized = hwVersion.toLowerCase().trim(); // Remove leading zeros (e.g., "0000070" -> "70", "0000060" -> "60") normalized = normalized.replace(/^0+/, ''); // If it starts with 'r', keep it as is (e.g., "r06a") if (normalized.startsWith('r')) { return normalized; } // Extract numeric part and suffix (e.g., "7a" -> "7" + "a", "70" -> "70" + "") const match = normalized.match(/^(\d+)([a-z]*)$/); if (!match) return 'r' + normalized; // Fallback let numPart = match[1]; const suffix = match[2]; // Pad single digit to 2 digits (e.g., "7" -> "07", "6" -> "06") if (numPart.length === 1) { numPart = '0' + numPart; } else if (numPart.length === 2 && numPart.endsWith('0') && numPart[0] !== '0') { // "70" -> "07", "60" -> "06" (swap if needed) numPart = '0' + numPart[0]; } // Add 'r' prefix (e.g., "07" -> "r07", "07a" -> "r07a", "07b" -> "r07b") return 'r' + numPart + suffix; } // Check if hardware version is restricted function isRestrictedHardware(hwVersion) { const normalized = normalizeHwVersion(hwVersion); const isRestricted = RESTRICTED_HW_VERSIONS.includes(normalized); console.log(`[HW Check] Original: "${hwVersion}" -> Normalized: "${normalized}" -> Restricted: ${isRestricted}`); return isRestricted; } // Helper function to generate country select options function generateCountryOptions(selectedCountry = '') { if (typeof COUNTRIES === 'undefined' || !COUNTRIES) { return ``; } // Sort countries alphabetically const sortedCountries = Object.values(COUNTRIES).sort((a, b) => { return a.country.localeCompare(b.country); }); let options = ''; sortedCountries.forEach(data => { const selected = (selectedCountry === data.country) ? 'selected' : ''; options += ``; }); return options; } // Serial port variables (port, writer, textEncoder, writableStreamClosed declared in PHP) let reader; let readableStreamClosed; let keepReading = true; // Browser compatibility check let isSerialSupported = false; // Check browser compatibility on page load function checkBrowserCompatibility() { isSerialSupported = 'serial' in navigator; if (!isSerialSupported) { // Show warning banner showBrowserWarningBanner(); // Disable connect button disableSerialFunctionality(); } return isSerialSupported; } function showBrowserWarningBanner() { const connectDevice = document.getElementById("connectdevice"); if (!connectDevice) return; const warningBanner = document.createElement("div"); warningBanner.id = "browserWarningBanner"; warningBanner.style.cssText = ` background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); color: white; padding: 15px 20px; border-radius: 8px; margin-bottom: 15px; display: flex; align-items: center; gap: 15px; box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); `; warningBanner.innerHTML = `
Browser Not Supported

Please use Chrome, Edge, or Opera to access device connectivity features.

`; connectDevice.parentNode.insertBefore(warningBanner, connectDevice); } function disableSerialFunctionality() { const connectButton = document.getElementById("connectButton"); if (connectButton) { connectButton.disabled = true; connectButton.style.opacity = "0.5"; connectButton.style.cursor = "not-allowed"; connectButton.title = "Browser is not supported. Please use Chrome, Edge, or Opera."; } } // Call on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkBrowserCompatibility); } else { checkBrowserCompatibility(); } // Shared serial port reference for upload.js to use window.sharedSerialPort = null; // Override requestPort to minimize user prompts // This intercepts all requestPort calls (including from upload.js) to reuse authorized ports if ('serial' in navigator) { const originalRequestPort = navigator.serial.requestPort.bind(navigator.serial); navigator.serial.requestPort = async function(options) { // If we have a shared port, return it instead of prompting if (window.sharedSerialPort) { console.log('Using shared serial port (no prompt needed)'); return window.sharedSerialPort; } // Try already-authorized ports matching the filters const ports = await navigator.serial.getPorts(); if (ports.length > 0 && options?.filters) { const match = ports.find(p => { const info = p.getInfo(); return options.filters.some(f => info.usbVendorId === f.usbVendorId && info.usbProductId === f.usbProductId ); }); if (match) { console.log('Using previously authorized port (no prompt needed)'); window.sharedSerialPort = match; return match; } } // Fallback: original prompt behavior const port = await originalRequestPort(options); window.sharedSerialPort = port; // Store for future use return port; }; } // Function to log communication to API (reused from scripts.js) async function logCommunication(data, direction) { // Only log if debug mode is enabled if (typeof DEBUG === 'undefined' || !DEBUG) { return; } try { const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; let serialNumber = ''; if (deviceSerialNumber) { serialNumber = deviceSerialNumber; } const logData = { data: data, direction: direction, timestamp: new Date().toISOString(), serial_number: serialNumber, maintenance_run: 0 }; const url = link + '/v2/com_log/log'; const bearer = 'Bearer ' + serviceToken; const response = await fetch(url, { method: 'POST', withCredentials: true, credentials: 'include', headers: { 'Authorization': bearer, 'Content-Type': 'application/json' }, body: JSON.stringify(logData) }); if (!response.ok) { console.warn('Failed to log communication:', response.status); } } catch (error) { console.warn('Error logging communication:', error); } } // Progress bar function (reused from scripts.js) function progressBar(percentage, message, color){ readBar.style.background = color; readBar.style.width = percentage +"%"; readBar.innerHTML = message; } // Connect device for software tool async function connectDeviceForSoftware() { // Browser compatibility check if (!isSerialSupported) { progressBar("100", "Browser not supported - Please use Chrome, Edge, or Opera", "#dc3545"); await logCommunication('Connection attempt failed: Web Serial API not supported in browser', 'error'); return; } //clear input readBar.innerHTML = ''; serialResultsDiv.innerHTML = ''; // Clear installation status if it exists const installStatus = document.getElementById("installationStatus"); if (installStatus) { installStatus.remove(); } document.getElementById("softwareCheckStatus").style.display = "none"; document.getElementById("softwareOptionsContainer").style.display = "none"; document.getElementById("noUpdatesMessage").style.display = "none"; document.getElementById("uploadSection").style.display = "none"; // Reset softwareOptions visibility and blur state const softwareOptions = document.getElementById("softwareOptions"); if (softwareOptions) { softwareOptions.style.display = "block"; softwareOptions.style.filter = "blur(8px)"; softwareOptions.style.opacity = "0.3"; softwareOptions.style.pointerEvents = "none"; } // Reset data receivedDataBuffer = ''; deviceSerialNumber = ""; deviceVersion = ""; deviceHwVersion = ""; // Clear the flag so popup shows for each new device connection // This allows users to connect multiple devices in the same session sessionStorage.removeItem('customerDataShownThisSession'); //set progress bar progressBar("1", "", ""); // Check if DEBUG mode is enabled - use mock device data if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) { // TEST MODE: Use mock device data deviceSerialNumber = "22110095"; deviceVersion = "03e615af"; deviceHwVersion = "0000070"; document.getElementById("Device_output").style.display = "block"; serialResultsDiv.innerHTML = `DEBUG MODE - Simulated Device Data:
SN=${deviceSerialNumber}
FW=${deviceVersion}
HW=${deviceHwVersion}`; progressBar("60", "DEBUG MODE - Device data read - SN: " + deviceSerialNumber, "#ff9800"); await logCommunication(`DEBUG MODE: Simulated device - SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake'); // Proceed to check software availability checkSoftwareAvailability(); return; } // Check if port is already open - if so, refresh the page to start clean if (port) { await logCommunication('Port already in use - refreshing page for clean reconnect', 'info'); location.reload(); return; } // Reset flags for new connection keepReading = true; try { // Prompt user to select any serial port. const filters = [{ usbVendorId: 1027, usbProductId: 24597 }]; port = await navigator.serial.requestPort({ filters }); // Log selected port details const portInfo = port.getInfo(); const portDetails = { processStep: 'Software', usbVendorId: portInfo.usbVendorId, usbProductId: portInfo.usbProductId, readable: !!port.readable, writable: !!port.writable, opened: port.readable !== null && port.writable !== null }; await logCommunication(`Selected USB device - ${JSON.stringify(portDetails)}`, 'connected'); await port.open({ baudRate: 56700, dataBits: 8, stopBits: 1, parity: 'none', flowControl: 'none' }); progressBar("10", "Connecting", "#04AA6D"); // Log successful connection await logCommunication('Port opened successfully', 'connected'); textEncoder = new TextEncoderStream(); writableStreamClosed = textEncoder.readable.pipeTo(port.writable); writer = textEncoder.writable.getWriter(); progressBar("20", "Connected, reading device...", "#04AA6D"); // Read device output await readDeviceOutput(); // Close the port immediately after reading await closePortAfterRead(); // Check for software updates (port is now closed) checkSoftwareAvailability(); } catch (error) { await logCommunication(`Connection error: ${error.message}`, 'error'); // Improved error messages with specific cases if (error.name === 'NotSupportedError' || !navigator.serial) { progressBar("100", "Browser not supported - Please use Chrome, Edge, or Opera", "#dc3545"); } else if (error.message && error.message.includes('No port selected by the user')) { progressBar("100", "No device selected - Please try again", "#ff6666"); } else if (error.name === 'NetworkError') { progressBar("100", "Connection failed - Please check device connection", "#ff6666"); } else if (error.name === 'InvalidStateError') { progressBar("100", "Port already in use - Refreshing page...", "#ff9800"); setTimeout(() => location.reload(), 2000); } else { progressBar("100", "Connection error: " + error.message, "#ff6666"); } } } async function readDeviceOutput() { const textDecoder = new TextDecoderStream(); readableStreamClosed = port.readable.pipeTo(textDecoder.writable); reader = textDecoder.readable.getReader(); document.getElementById("Device_output").style.display = "block"; let dataCompleteMarkerFound = false; const startTime = Date.now(); const timeout = 10000; // 10 second timeout try { progressBar("40", "Reading device...", "#04AA6D"); while (keepReading) { const { value, done } = await reader.read(); if (done || !keepReading) { break; } receivedDataBuffer += value; serialResultsDiv.innerHTML = receivedDataBuffer; // Check if we have received PLUGTY which comes after all device data // Or wait for at least FWDATE which comes near the end if (receivedDataBuffer.indexOf("PLUGTY") > 0 || receivedDataBuffer.indexOf("FWDATE=") > 0) { dataCompleteMarkerFound = true; } // Only parse and exit if we found the completion marker if (dataCompleteMarkerFound) { // Parse device info from complete output parseDeviceInfo(receivedDataBuffer); // Check if we have all required data if (deviceSerialNumber && deviceVersion && deviceHwVersion) { progressBar("60", "Device data read - SN: " + deviceSerialNumber, "#04AA6D"); await logCommunication(`Device identification data received: SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake'); // Exit cleanly break; } } // Timeout check if (Date.now() - startTime > timeout) { await logCommunication('Device read timeout - proceeding with partial data', 'error'); parseDeviceInfo(receivedDataBuffer); break; } } progressBar("65", "Device reading completed", "#04AA6D"); } catch (error) { if (keepReading) { await logCommunication(`Read error: ${error.message}`, 'error'); progressBar("0", "Error reading device: " + error.message, "#ff6666"); } } finally { // Clean up reader and cancel the stream try { if (reader) { await reader.cancel(); reader.releaseLock(); } } catch (e) { console.log('Reader cleanup error:', e); } } } function parseDeviceInfo(data) { // Extract SN, FW (firmware/software version), and HW from device output // Device format: SN=12345678;FW=12345678;HW=12345678;STATE=... // Match exact logic from scripts.js and readdevice.js // Use exact same approach as scripts.js line 153 and readdevice.js line 649 const x = Array.from(new Set(data.split(";"))).toString(); // Parse SN= (8 characters, exact logic from scripts.js line 185-189) if (x.indexOf("SN=") > 0 && !deviceSerialNumber) { const a = x.indexOf("SN="); const b = a + 3; const c = b + 8; deviceSerialNumber = x.substring(b, c); console.log("Found SN:", deviceSerialNumber); } // Parse FW= (8 characters, exact logic from readdevice.js line 672-676) if (x.indexOf("FW=") > 0 && !deviceVersion) { const a = x.indexOf("FW="); const b = a + 3; const c = b + 8; deviceVersion = x.substring(b, c); console.log("Found FW/Version:", deviceVersion); } // Parse HW= (8 characters, exact logic from readdevice.js line 665-670) if (x.indexOf("HW=") > 0 && !deviceHwVersion) { const a = x.indexOf("HW="); const b = a + 3; const c = b + 8; deviceHwVersion = x.substring(b, c); console.log("Found HW Version:", deviceHwVersion); } } async function closePortAfterRead() { if (port) { try { await logCommunication('Closing port after reading device data', 'info'); // Reader is already cancelled and released by readDeviceOutput finally block // Now abort the writer if (writer) { try { await writer.abort(); } catch (e) { console.log('Writer abort error:', e); } } // Give time for streams to cleanup await new Promise(resolve => setTimeout(resolve, 300)); // Close the port await port.close(); await logCommunication('Port closed successfully', 'info'); // Keep port reference in sharedSerialPort for upload.js to reuse // This prevents the need for another user prompt during firmware upload window.sharedSerialPort = port; // Reset local variables for next connection reader = null; writer = null; readableStreamClosed = null; writableStreamClosed = null; port = null; } catch (error) { console.error('Error closing port after read:', error); await logCommunication(`Error closing port: ${error.message}`, 'error'); // Keep port reference even on error if port exists if (port) { window.sharedSerialPort = port; } // Force reset local variables even on error reader = null; writer = null; readableStreamClosed = null; writableStreamClosed = null; port = null; } } } async function closePort() { if (port) { try { keepReading = false; // Wait for read operations to complete await new Promise(resolve => setTimeout(resolve, 300)); // Release reader if still locked if (reader) { try { reader.releaseLock(); } catch (e) { console.log('Reader release error:', e); } } // Wait a bit more await new Promise(resolve => setTimeout(resolve, 200)); // Now close the port try { await port.close(); await logCommunication('Port closed', 'info'); } catch (e) { console.log('Port close error (may already be closed):', e); } // Reset for next connection reader = null; port = null; } catch (error) { console.error('Error closing port:', error); } } } async function checkSoftwareAvailability() { if (!deviceSerialNumber) { progressBar("0", "Error: Serial number not found", "#ff6666"); await logCommunication('Serial number not found in device output', 'error'); return; } document.getElementById("softwareCheckStatus").style.display = "block"; progressBar("70", "Checking for software updates...", "#04AA6D"); try { // Call software_available API const availableUrl = link + "/v2/software_available/sn=" + deviceSerialNumber + (deviceVersion ? "&version=" + deviceVersion : "") + (deviceHwVersion ? "&hw_version=" + deviceHwVersion : ""); console.log("Calling API:", availableUrl); await logCommunication(`Checking software availability for SN: ${deviceSerialNumber}`, 'sent'); const availableResponse = await fetch(availableUrl, { method: "GET", headers: { "Authorization": "Bearer " + document.getElementById("servicetoken").textContent, "Content-Type": "application/json" } }); const availableData = await availableResponse.json(); console.log("Available response:", availableData); await logCommunication(`Software availability response: ${JSON.stringify(availableData)}`, 'received'); if (availableData.software_available === "error" || availableData.error) { // Error checking for updates (e.g., invalid hardware version) document.getElementById("softwareCheckStatus").style.display = "none"; progressBar("0", "Error: " + (availableData.error || "Unable to check for updates"), "#ff6666"); alert("Error checking for updates: " + (availableData.error || "Unknown error")); } else if (availableData.software_available === "yes") { // Software updates available, fetch options progressBar("80", "Software updates found, loading options...", "#04AA6D"); await fetchSoftwareOptions(); } else { // No updates available document.getElementById("softwareCheckStatus").style.display = "none"; document.getElementById("noUpdatesMessage").style.display = "block"; progressBar("100", "No software updates available", "#04AA6D"); } } catch (error) { await logCommunication(`Software check error: ${error.message}`, 'error'); progressBar("0", "Error checking software: " + error.message, "#ff6666"); await closePort(); } } async function fetchSoftwareOptions() { try { // Call software_update API to get options const updateUrl = link + "/v2/software_update/sn=" + deviceSerialNumber + (deviceVersion ? "&version=" + deviceVersion : "") + (deviceHwVersion ? "&hw_version=" + deviceHwVersion : ""); console.log("Fetching options from:", updateUrl); await logCommunication(`Fetching software options for SN: ${deviceSerialNumber}`, 'sent'); const updateResponse = await fetch(updateUrl, { method: "GET", headers: { "Authorization": "Bearer " + document.getElementById("servicetoken").textContent, "Content-Type": "application/json" } }); const options = await updateResponse.json(); console.log("Software options:", options); await logCommunication(`Software options response: ${JSON.stringify(options)}`, 'received'); if (options.error) { document.getElementById("softwareCheckStatus").style.display = "none"; document.getElementById("noUpdatesMessage").style.display = "block"; progressBar("100", "No software updates available", "#04AA6D"); await closePort(); return; } // Display options in table (blurred initially) displaySoftwareOptions(options); document.getElementById("softwareCheckStatus").style.display = "none"; document.getElementById("softwareOptionsContainer").style.display = "block"; progressBar("100", "Software options loaded", "#04AA6D"); // Check if we're returning from a payment const urlParams = new URLSearchParams(window.location.search); const isReturningFromPayment = urlParams.has('payment_return') || urlParams.has('order_id'); // Check if customer data was already shown in THIS session (not from previous session) const customerDataShownThisSession = sessionStorage.getItem('customerDataShownThisSession'); // Show user info modal unless: // 1. We're returning from payment // 2. We already showed the modal in this session if (!isReturningFromPayment && !customerDataShownThisSession) { // Always show userInfoModal (even in debug mode) showUserInfoModal(); } else { // Returning from payment or already shown this session console.log('[HW Check] Skipping userInfoModal - checking hardware version...'); console.log('[HW Check] deviceHwVersion:', deviceHwVersion); // Check if hardware version is restricted if (isRestrictedHardware(deviceHwVersion)) { // Show hardware warning modal console.log('[HW Check] Hardware is restricted - showing warning modal'); showHwWarningModal(); } else { // Reveal software options immediately console.log('[HW Check] Hardware is not restricted - revealing options'); const softwareOptions = document.getElementById("softwareOptions"); if (softwareOptions) { softwareOptions.style.filter = "none"; softwareOptions.style.opacity = "1"; softwareOptions.style.pointerEvents = "auto"; } } } } catch (error) { await logCommunication(`Software options error: ${error.message}`, 'error'); progressBar("0", "Error loading options: " + error.message, "#ff6666"); await closePort(); } } function displaySoftwareOptions(options) { const grid = document.getElementById("softwareOptionsGrid"); grid.innerHTML = ""; options.forEach((option, index) => { const price = parseFloat(option.price); const isFree = price === 0; const isCurrent = option.is_current === true || option.is_current === 1; const dealerInfo = option.dealer_info || {}; const isDealer = dealerInfo.is_dealer === 1 || dealerInfo.is_dealer === '1'; // Create card with gradient background const card = document.createElement("div"); card.style.cssText = ` background: ${isCurrent ? 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' : 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'}; border-radius: 4px; padding: 25px 20px; transition: all 0.3s ease; display: flex; flex-direction: column; position: relative; overflow: visible; transform: translateY(0px); box-shadow: ${isCurrent ? '0 4px 12px rgba(0,0,0,0.08)' : '0 8px 20px rgba(0,0,0,0.12)'}; opacity: ${isCurrent ? '0.7' : '1'}; pointer-events: ${isCurrent ? 'none' : 'auto'}; min-height: 320px; `; if (!isCurrent) { card.onmouseenter = () => { card.style.transform = 'translateY(-8px) scale(1.02)'; card.style.boxShadow = '0 12px 28px rgba(0,0,0,0.2)'; card.style.borderColor = isFree ? '#038f5a' : '#FF4500'; }; card.onmouseleave = () => { card.style.transform = 'translateY(0) scale(1)'; card.style.boxShadow = '0 8px 20px rgba(0,0,0,0.12)'; card.style.borderColor = isFree ? '#04AA6D' : '#FF6B35'; }; } // Badge for current/free/paid - VISIBLE const badge = document.createElement("div"); badge.style.cssText = ` position: absolute; top: -10px; right: 20px; background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')}; color: white; padding: 8px 16px; border-radius: 20px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; if (isCurrent) { badge.textContent = "INSTALLED"; } else if (isFree) { badge.textContent = "FREE"; } else { badge.textContent = "PREMIUM"; } card.appendChild(badge); // Name with icon const name = document.createElement("h4"); name.style.cssText = ` margin: 0 0 12px 0; color: #333; font-size: 22px; font-weight: 700; `; name.innerHTML = `${option.name || "Software Update"}`; card.appendChild(name); // Version with enhanced styling const version = document.createElement("div"); version.style.cssText = ` color: #666; font-size: 14px; margin-bottom: 15px; display: flex; align-items: center; gap: 6px; `; version.innerHTML = ` Version: ${option.version || "N/A"}`; card.appendChild(version); // Description with preserved newlines const descContainer = document.createElement("div"); descContainer.style.cssText = ` color: #555; font-size: 13px; line-height: 1.7; margin: 0 0 20px 0; flex-grow: 1; white-space: pre-line; `; descContainer.textContent = option.description || "No description available"; card.appendChild(descContainer); // Price section const priceSection = document.createElement("div"); priceSection.style.cssText = ` border-top: 2px solid ${isFree ? '#04AA6D20' : '#FF6B3520'}; padding-top: 20px; margin-top: auto; `; // Check if this is a dealer customer - show dealer contact info instead of price/buy button if (isDealer && !isCurrent && !isFree) { // Dealer info section - replaces price and buy button const dealerSection = document.createElement("div"); dealerSection.style.cssText = ` background: linear-gradient(135deg, rgb(255, 107, 53) 0%, rgb(255, 69, 0) 100%); border-radius: 4px; padding: 15px; text-align: center; `; // Contact dealer message const dealerMessage = document.createElement("div"); dealerMessage.style.cssText = ` color: white; font-size: 14px; font-weight: 600; margin-bottom: 12px; `; dealerMessage.innerHTML = 'Contact your dealer for pricing and upgrade options'; dealerSection.appendChild(dealerMessage); // Dealer contact details const dealerDetails = document.createElement("div"); dealerDetails.style.cssText = ` background: white; border-radius: 4px; padding: 12px; text-align: left; font-size: 13px; color: #333; `; let dealerHtml = ''; if (dealerInfo.name) { dealerHtml += `
${dealerInfo.name}
`; } if (dealerInfo.address || dealerInfo.city || dealerInfo.postalcode || dealerInfo.country) { dealerHtml += `
`; if (dealerInfo.address) { dealerHtml += `
${dealerInfo.address}
`; } if (dealerInfo.postalcode || dealerInfo.city) { dealerHtml += `
${[dealerInfo.postalcode, dealerInfo.city].filter(Boolean).join(' ')}
`; } if (dealerInfo.country) { dealerHtml += `
${dealerInfo.country}
`; } dealerHtml += `
`; } if (dealerInfo.email) { dealerHtml += `
${dealerInfo.email}
`; } if (dealerInfo.phone) { dealerHtml += `
${dealerInfo.phone}
`; } dealerDetails.innerHTML = dealerHtml; dealerSection.appendChild(dealerDetails); priceSection.appendChild(dealerSection); } else { // Standard price display for non-dealer customers const priceText = document.createElement("div"); priceText.style.cssText = ` font-size: ${isCurrent ? '18px' : '28px'}; font-weight: ${isCurrent ? '600' : '800'}; color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#FF6B35')}; margin-bottom: 15px; text-align: center; letter-spacing: 0.5px; `; if (isCurrent) { priceText.innerHTML = ' INSTALLED'; } else { priceText.innerHTML = isFree ? 'Free' : `${option.currency || "€"} ${price.toFixed(2)} (excl. VAT)`; } priceSection.appendChild(priceText); // Action button with gradient for paid const actionBtn = document.createElement("button"); actionBtn.className = "btn"; actionBtn.style.cssText = ` width: 100%; background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')}; color: white; border: none; cursor: ${isCurrent ? 'not-allowed' : 'pointer'}; transition: all 0.3s ease; opacity: ${isCurrent ? '0.5' : '1'}; box-shadow: ${isCurrent ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'}; letter-spacing: 0.5px; text-transform: uppercase; `; if (isCurrent) { actionBtn.innerHTML = ' Currently Installed'; actionBtn.disabled = true; } else if (isFree) { actionBtn.innerHTML = ''; actionBtn.onclick = () => selectUpgrade(option); actionBtn.onmouseenter = () => { actionBtn.style.background = 'linear-gradient(135deg, #038f5a 0%, #026b43 100%)'; actionBtn.style.transform = 'translateY(-2px)'; actionBtn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.2)'; }; actionBtn.onmouseleave = () => { actionBtn.style.background = 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)'; actionBtn.style.transform = 'translateY(0)'; actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; }; } else { actionBtn.innerHTML = ''; actionBtn.onclick = () => selectUpgrade(option); actionBtn.onmouseenter = () => { actionBtn.style.background = 'linear-gradient(135deg, #FF4500 0%, #CC3700 100%)'; actionBtn.style.transform = 'translateY(-2px)'; actionBtn.style.boxShadow = '0 6px 16px rgba(255,107,53,0.4)'; }; actionBtn.onmouseleave = () => { actionBtn.style.background = 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)'; actionBtn.style.transform = 'translateY(0)'; actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; }; } priceSection.appendChild(actionBtn); } card.appendChild(priceSection); grid.appendChild(card); }); } function showUserInfoModal() { // Create modal overlay const modal = document.createElement("div"); modal.id = "userInfoModal"; modal.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center; `; // Create modal content const modalContent = document.createElement("div"); modalContent.style.cssText = ` background: white; border-radius: 12px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; margin: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); `; modalContent.innerHTML = `

${typeof TRANS_USER_INFO_REQUIRED !== 'undefined' ? TRANS_USER_INFO_REQUIRED : 'User Information Required'}

${typeof TRANS_USER_INFO_DESCRIPTION !== 'undefined' ? TRANS_USER_INFO_DESCRIPTION : 'Please provide your information to continue with software updates'}

`; modal.appendChild(modalContent); document.body.appendChild(modal); // Prefill form with customer data from sessionStorage if available const savedCustomerData = sessionStorage.getItem('customerData'); if (savedCustomerData) { try { const customerData = JSON.parse(savedCustomerData); if (customerData.name) document.getElementById("userInfoName").value = customerData.name; if (customerData.email) document.getElementById("userInfoEmail").value = customerData.email; if (customerData.address) document.getElementById("userInfoAddress").value = customerData.address; if (customerData.city) document.getElementById("userInfoCity").value = customerData.city; if (customerData.postal) document.getElementById("userInfoPostal").value = customerData.postal; if (customerData.country) document.getElementById("userInfoCountry").value = customerData.country; } catch (e) { console.warn('Error parsing saved customer data:', e); } } // Handle form submission document.getElementById("userInfoForm").onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const customerData = { name: formData.get("name"), email: formData.get("email"), address: formData.get("address"), city: formData.get("city"), postal: formData.get("postal"), country: formData.get("country") }; // Save customer data to sessionStorage sessionStorage.setItem('customerData', JSON.stringify(customerData)); // Mark that we've shown the customer data modal in this session sessionStorage.setItem('customerDataShownThisSession', 'true'); // Send to API await sendUserInfoToAPI(customerData); // Close modal document.body.removeChild(modal); // Check if hardware version is restricted if (isRestrictedHardware(deviceHwVersion)) { // Show hardware warning modal showHwWarningModal(); } else { // Reveal software options by removing blur const softwareOptions = document.getElementById("softwareOptions"); if (softwareOptions) { softwareOptions.style.filter = "none"; softwareOptions.style.opacity = "1"; softwareOptions.style.pointerEvents = "auto"; } } }; } async function sendUserInfoToAPI(customerData) { try { const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; const url = link + '/v2/history'; const bearer = 'Bearer ' + serviceToken; const historyData = { sn: deviceSerialNumber, type: 'customer', sn_service: 'Portal', payload: customerData }; await logCommunication(`Sending user info to API: ${JSON.stringify(historyData)}`, 'sent'); const response = await fetch(url, { method: 'POST', withCredentials: true, credentials: 'include', headers: { 'Authorization': bearer, 'Content-Type': 'application/json' }, body: JSON.stringify(historyData) }); if (!response.ok) { console.warn('Failed to send user info:', response.status); await logCommunication(`Failed to send user info: ${response.status}`, 'error'); } else { const result = await response.json(); console.log("User info sent successfully:", result); await logCommunication(`User info sent successfully: ${JSON.stringify(result)}`, 'received'); } } catch (error) { console.warn('Error sending user info:', error); await logCommunication(`Error sending user info: ${error.message}`, 'error'); } } function showHwWarningModal() { console.log(`[HW Check] Showing hardware warning modal for HW version: ${deviceHwVersion}`); // Create modal overlay - matching userInfoModal const modal = document.createElement("div"); modal.id = "hwWarningModal"; modal.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center; `; // Create modal content - matching userInfoModal const modalContent = document.createElement("div"); modalContent.style.cssText = ` background: white; border-radius: 12px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; margin: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); `; modalContent.innerHTML = `

${typeof TRANS_HW_WARNING_TITLE !== 'undefined' ? TRANS_HW_WARNING_TITLE : 'Hardware Compatibility Notice'}

${typeof TRANS_HW_WARNING_SUBTITLE !== 'undefined' ? TRANS_HW_WARNING_SUBTITLE : 'Please read the following information carefully'}

${typeof TRANS_HW_WARNING_DETECTED !== 'undefined' ? TRANS_HW_WARNING_DETECTED.replace('{hw_version}', deviceHwVersion) : `Hardware version ${deviceHwVersion} detected`}

${typeof TRANS_HW_WARNING_TEXT !== 'undefined' ? TRANS_HW_WARNING_TEXT : 'This hardware version requires special attention. Please ensure you select the correct software version for your device.'}

`; modal.appendChild(modalContent); document.body.appendChild(modal); // Handle continue button click document.getElementById("hwWarningContinue").onclick = () => { // Close modal document.body.removeChild(modal); // Reveal software options by removing blur const softwareOptions = document.getElementById("softwareOptions"); if (softwareOptions) { softwareOptions.style.filter = "none"; softwareOptions.style.opacity = "1"; softwareOptions.style.pointerEvents = "auto"; } }; } async function selectUpgrade(option) { const price = parseFloat(option.price || 0); const isFree = price === 0; // If paid upgrade, show payment modal with pre-filled data if (!isFree) { showPaymentModal(option); return; } // Free upgrade - proceed directly with saved customer data const savedCustomerData = sessionStorage.getItem('customerData'); if (savedCustomerData) { try { const customerData = JSON.parse(savedCustomerData); await downloadAndInstallSoftware(option, customerData); } catch (e) { console.warn('Error parsing saved customer data:', e); showFreeInstallModal(option); } } else { showFreeInstallModal(option); } } function showFreeInstallModal(option) { // Create modal overlay const modal = document.createElement("div"); modal.id = "freeInstallModal"; modal.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; `; // Create modal content const modalContent = document.createElement("div"); modalContent.style.cssText = ` background: white; border-radius: 8px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); `; modalContent.innerHTML = `

Confirm Software Installation

${option.name || "Software Update"}

Version: ${option.version || "N/A"}

${option.description || ""}

`; modal.appendChild(modalContent); document.body.appendChild(modal); // Prefill form with customer data from sessionStorage if available const savedCustomerData = sessionStorage.getItem('customerData'); if (savedCustomerData) { try { const customerData = JSON.parse(savedCustomerData); if (customerData.name) document.getElementById("freeInstallName").value = customerData.name; if (customerData.email) document.getElementById("freeInstallEmail").value = customerData.email; if (customerData.address) document.getElementById("freeInstallAddress").value = customerData.address; if (customerData.city) document.getElementById("freeInstallCity").value = customerData.city; if (customerData.postal) document.getElementById("freeInstallPostal").value = customerData.postal; if (customerData.country) document.getElementById("freeInstallCountry").value = customerData.country; } catch (e) { console.warn('Error parsing saved customer data:', e); } } // Close modal on cancel document.getElementById("cancelFreeInstall").onclick = () => { document.body.removeChild(modal); }; // Handle form submission document.getElementById("freeInstallForm").onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const customerData = { name: formData.get("name"), email: formData.get("email"), address: formData.get("address"), city: formData.get("city"), postal: formData.get("postal"), country: formData.get("country"), version_id: option.version_id, price: 0, currency: option.currency || "€" }; // Save customer data to sessionStorage for future use sessionStorage.setItem('customerData', JSON.stringify({ name: customerData.name, email: customerData.email, address: customerData.address, city: customerData.city, postal: customerData.postal, country: customerData.country })); // Close modal document.body.removeChild(modal); // Proceed to download and install with customer data await downloadAndInstallSoftware(option, customerData); }; } function showPaymentModal(option) { const price = parseFloat(option.price || 0); const currency = option.currency || "€"; // Format description as bullet points const formatDescription = (desc) => { if (!desc) return ''; // Split by bullet points or newlines and filter out empty lines const lines = desc.split(/[•·\n]/).map(line => line.trim()).filter(line => line.length > 0); if (lines.length <= 1) return desc; // Return as-is if no multiple lines return ''; }; // Create modal overlay const modal = document.createElement("div"); modal.id = "paymentModal"; modal.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; `; // Create modal content const modalContent = document.createElement("div"); modalContent.style.cssText = ` background: white; border-radius: 8px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); `; modalContent.innerHTML = `

Purchase Software Upgrade

${option.name || "Software Update"}

Version: ${option.version || "N/A"}

${formatDescription(option.description)}
Price (excl. VAT): ${currency} ${price.toFixed(2)}
VAT: -
Total: ${currency} ${price.toFixed(2)}
`; modal.appendChild(modalContent); document.body.appendChild(modal); // VAT number validation state let vatValidationInProgress = false; let vatValidationResult = null; // Function to check VAT number against VIES database (via server-side proxy) async function checkVATNumber(countryCode, vatNumber) { if (!countryCode || !vatNumber) { return null; } // Use server-side proxy to avoid CORS issues const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; const url = link + '/v2/vat_check'; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer ' + serviceToken }, body: JSON.stringify({ countryCode: countryCode, vatNumber: vatNumber }) }); if (!response.ok) { console.warn('VAT check HTTP error:', response.status); return null; } const data = await response.json(); console.log('VIES response:', data); return data; } catch (error) { console.error('Error checking VAT:', error); return null; } } // Function to calculate and update tax function updateTaxDisplay() { const selectedCountry = document.getElementById("paymentCountry").value; const vatNumber = document.getElementById("paymentVatNumber").value.trim(); let taxRate = 0; let vatNote = ''; if (selectedCountry && typeof COUNTRIES !== 'undefined' && COUNTRIES) { const countryData = Object.values(COUNTRIES).find(c => c.country === selectedCountry); if (countryData) { const isEU = countryData.eu === 1; const isNetherlands = selectedCountry === 'Netherlands'; const countryTaxRate = parseFloat(countryData.taxes) || 0; if (isNetherlands) { // Netherlands: always take the tax percentage taxRate = countryTaxRate; } else if (isEU) { if (vatNumber && vatValidationResult && vatValidationResult.valid === true) { // EU with VALID VAT number: 0% VAT, reverse charge taxRate = 0; vatNote = 'Reverse charge VAT'; } else { // EU without VAT number or invalid VAT: use country VAT taxRate = countryTaxRate; vatNote = 'Local VAT'; } } else { // Non-EU: use country tax percentage (usually 0) taxRate = countryTaxRate; vatNote = 'Out of scope of EU VAT. Buyer is responsible for local taxes and duties'; } } } const taxAmount = price * (taxRate / 100); const totalAmount = price + taxAmount; // Update display const taxDisplay = document.getElementById("taxDisplay"); const totalDisplay = document.getElementById("totalDisplay"); if (taxRate > 0) { taxDisplay.innerHTML = ` VAT (${taxRate}%)${vatNote ? ' (' + vatNote + ')' : ''}: ${currency} ${taxAmount.toFixed(2)} `; } else { taxDisplay.innerHTML = ` VAT${vatNote ? ' (' + vatNote + ')' : ''}: ${vatNote ? '0%' : '-'} `; } totalDisplay.textContent = `${currency} ${totalAmount.toFixed(2)}`; // Store tax info for form submission modal.taxRate = taxRate; modal.taxAmount = taxAmount; modal.totalAmount = totalAmount; } // Prefill form with customer data from sessionStorage if available const savedCustomerData = sessionStorage.getItem('customerData'); if (savedCustomerData) { try { const customerData = JSON.parse(savedCustomerData); if (customerData.name) document.getElementById("paymentName").value = customerData.name; if (customerData.email) document.getElementById("paymentEmail").value = customerData.email; if (customerData.address) document.getElementById("paymentAddress").value = customerData.address; if (customerData.city) document.getElementById("paymentCity").value = customerData.city; if (customerData.postal) document.getElementById("paymentPostal").value = customerData.postal; if (customerData.vat_number) document.getElementById("paymentVatNumber").value = customerData.vat_number; if (customerData.country) { document.getElementById("paymentCountry").value = customerData.country; updateTaxDisplay(); // Calculate tax based on saved country } } catch (e) { console.warn('Error parsing saved customer data:', e); } } // Debounce timer for VAT validation let vatValidationTimeout = null; // Function to validate VAT number with visual feedback async function validateVATNumber() { const selectedCountry = document.getElementById("paymentCountry").value; const vatNumber = document.getElementById("paymentVatNumber").value.trim(); const vatInput = document.getElementById("paymentVatNumber"); // Reset validation state vatValidationResult = null; vatInput.style.borderColor = ''; // Remove any existing validation message const existingMessage = document.getElementById('vatValidationMessage'); if (existingMessage) { existingMessage.remove(); } if (!vatNumber) { updateTaxDisplay(); return; } if (!selectedCountry || typeof COUNTRIES === 'undefined' || !COUNTRIES) { updateTaxDisplay(); return; } const countryData = Object.values(COUNTRIES).find(c => c.country === selectedCountry); if (!countryData || countryData.eu !== 1 || !countryData.country_code) { updateTaxDisplay(); return; } // For Netherlands, don't validate VAT (always apply VAT) if (selectedCountry === 'Netherlands') { updateTaxDisplay(); return; } // Show validating state vatInput.style.borderColor = '#ffc107'; vatValidationInProgress = true; const validationMsg = document.createElement('div'); validationMsg.id = 'vatValidationMessage'; validationMsg.style.cssText = 'margin-top: 5px; font-size: 12px; color: #ffc107;'; validationMsg.innerHTML = ' Validating VAT number...'; vatInput.parentNode.appendChild(validationMsg); // Call VIES API const result = await checkVATNumber(countryData.country_code, vatNumber); vatValidationInProgress = false; if (result && result.valid === true) { // VAT number is valid vatValidationResult = result; vatInput.style.borderColor = '#28a745'; validationMsg.style.color = '#28a745'; validationMsg.innerHTML = ' Valid VAT number'; // Format VAT number as CountryCode + VatNumber (e.g., DE115235681) const formattedVAT = result.countryCode + result.vatNumber; if (vatInput.value !== formattedVAT) { vatInput.value = formattedVAT; } } else { // VAT number is invalid or check failed vatValidationResult = null; vatInput.style.borderColor = '#dc3545'; validationMsg.style.color = '#dc3545'; validationMsg.innerHTML = ' Invalid VAT number or validation failed'; } // Update tax display with new validation result updateTaxDisplay(); } // Add event listeners to country select and VAT number to update tax document.getElementById("paymentCountry").addEventListener('change', () => { vatValidationResult = null; const vatInput = document.getElementById("paymentVatNumber"); vatInput.style.borderColor = ''; const existingMessage = document.getElementById('vatValidationMessage'); if (existingMessage) { existingMessage.remove(); } updateTaxDisplay(); // Validate VAT if already entered if (vatInput.value.trim()) { if (vatValidationTimeout) { clearTimeout(vatValidationTimeout); } vatValidationTimeout = setTimeout(validateVATNumber, 500); } }); document.getElementById("paymentVatNumber").addEventListener('input', () => { // Debounce VAT validation if (vatValidationTimeout) { clearTimeout(vatValidationTimeout); } vatValidationTimeout = setTimeout(validateVATNumber, 1000); }); // Close modal on cancel document.getElementById("cancelPayment").onclick = () => { document.body.removeChild(modal); }; // Handle form submission document.getElementById("paymentForm").onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const paymentMethod = formData.get("payment_method"); // Auto-determine payment provider based on payment method let paymentProvider = 'mollie'; // default if (paymentMethod === '3') { // PayPal payment method ID paymentProvider = 'paypal'; } else if (paymentMethod === '1' || paymentMethod === 'bank_transfer') { // Mollie (Credit Card) or Bank Transfer paymentProvider = 'mollie'; } const paymentData = { name: formData.get("name"), email: formData.get("email"), address: formData.get("address"), city: formData.get("city"), postal: formData.get("postal"), country: formData.get("country"), vat_number: formData.get("vat_number") || '', payment_method: paymentMethod, payment_provider: paymentProvider, version_id: option.version_id, item_price: price, // Price without VAT tax_amount: modal.taxAmount || 0, // Tax amount payment_amount: modal.totalAmount || price, // Total price including tax currency: currency }; // Save customer data to sessionStorage for future use sessionStorage.setItem('customerData', JSON.stringify({ name: paymentData.name, email: paymentData.email, address: paymentData.address, city: paymentData.city, postal: paymentData.postal, country: paymentData.country, vat_number: paymentData.vat_number })); await processPayment(paymentData, option, modal); }; } async function logSoftwareInstallationToHistory(option, customerData = null) { try { const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; // Create payload matching the structure expected by v2/history const payload = { version: option.version, version_id: option.version_id, name: option.name, description: option.description, HW: deviceHwVersion, HEX_FW: option.version, price: option.price || 0, currency: option.currency || "€", installation_source: 'softwaretool' }; // Add customer data if provided if (customerData) { payload.customer = { name: customerData.name, email: customerData.email, address: customerData.address, city: customerData.city, postal: customerData.postal, country: customerData.country }; } const historyData = { sn: deviceSerialNumber, type: 'firmware', sn_service: 'Portal', payload: payload }; const url = link + '/v2/history'; const bearer = 'Bearer ' + serviceToken; console.log("Logging installation to history:", historyData); await logCommunication(`Logging software installation to history: ${JSON.stringify(historyData)}`, 'sent'); const response = await fetch(url, { method: 'POST', withCredentials: true, credentials: 'include', headers: { 'Authorization': bearer, 'Content-Type': 'application/json' }, body: JSON.stringify(historyData) }); if (!response.ok) { console.warn('Failed to log to history:', response.status); await logCommunication(`Failed to log to history: ${response.status}`, 'error'); } else { const result = await response.json(); console.log("History logged successfully:", result); await logCommunication(`History logged successfully: ${JSON.stringify(result)}`, 'received'); } } catch (error) { console.warn('Error logging to history:', error); await logCommunication(`Error logging to history: ${error.message}`, 'error'); // Don't block the installation if history logging fails } } async function processPayment(paymentData, option, modal) { try { progressBar("10", "Processing payment...", "#04AA6D"); // SECURITY: Only send serial_number and version_id // Server will calculate price to prevent tampering const paymentRequest = { serial_number: deviceSerialNumber, version_id: option.version_id, payment_method: paymentData.payment_method, payment_provider: paymentData.payment_provider, user_data: paymentData // name, email, address, etc. }; // Debug logging if (typeof DEBUG !== 'undefined' && DEBUG) { console.log("=== DEBUG: Payment Request ==="); console.log("Serial Number:", deviceSerialNumber); console.log("Version ID:", option.version_id); console.log("Payment Method:", paymentData.payment_method); console.log("Payment Provider:", paymentData.payment_provider); console.log("User Data:", paymentData); console.log("Request payload:", paymentRequest); } await logCommunication(`Payment initiated for version ${option.version_id} via ${paymentData.payment_provider}`, 'sent'); // Call payment API (handles both Mollie and PayPal) const response = await fetch(link + "/v2/payment", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + document.getElementById("servicetoken").textContent }, body: JSON.stringify(paymentRequest) }); if (!response.ok) { const errorData = await response.json(); if (typeof DEBUG !== 'undefined' && DEBUG) { console.error("DEBUG: Payment API error:", errorData); } throw new Error(errorData.error || "Failed to create payment"); } const result = await response.json(); if (typeof DEBUG !== 'undefined' && DEBUG) { console.log("=== DEBUG: Payment Response ==="); console.log("Result:", result); console.log("Checkout URL:", result.checkout_url); console.log("Payment ID:", result.payment_id); } if (result.checkout_url) { await logCommunication(`Redirecting to ${paymentData.payment_provider} payment: ${result.payment_id}`, 'sent'); if (typeof DEBUG !== 'undefined' && DEBUG) { console.log(`DEBUG: Redirecting to ${paymentData.payment_provider} checkout...`); } // Close modal before redirect document.body.removeChild(modal); // Redirect to payment checkout page window.location.href = result.checkout_url; } else { throw new Error(result.error || "No checkout URL received"); } } catch (error) { if (typeof DEBUG !== 'undefined' && DEBUG) { console.error("DEBUG: Payment processing error:", error); } await logCommunication(`Payment error: ${error.message}`, 'error'); progressBar("0", "Payment failed: " + error.message, "#ff6666"); alert("Payment failed: " + error.message); } } async function downloadAndInstallSoftware(option, customerData = null) { //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // SECURITY: Check if returning from payment - verify serial number //+++++++++++++++++++++++++++++++++++++++++++++++++++++ const urlParams = new URLSearchParams(window.location.search); const paymentId = urlParams.get('payment_id'); if (paymentId) { try { // Verify serial number matches payment const response = await fetch(link + `/v2/payment?payment_id=${paymentId}`, { method: "GET", headers: { "Authorization": "Bearer " + document.getElementById("servicetoken").textContent } }); if (response.ok) { const paymentData = await response.json(); // Check if device serial number matches the one from payment if (paymentData.serial_number && paymentData.serial_number !== deviceSerialNumber) { const confirmed = confirm( `WARNING: Different device detected!\n\n` + `License was created for device: ${paymentData.serial_number}\n` + `Currently connected device: ${deviceSerialNumber}\n\n` + `The license is already applied to the original device. ` + `Do you want to continue with this device anyway?` ); if (!confirmed) { progressBar("0", "Upload canceled by user", "#ff6666"); await logCommunication('Upload canceled - serial number mismatch', 'error'); return; } await logCommunication(`WARNING: Serial number mismatch detected (payment: ${paymentData.serial_number}, device: ${deviceSerialNumber})`, 'warning'); } } } catch (error) { console.error("Error verifying payment:", error); // Don't block if verification fails - log and continue await logCommunication(`Payment verification failed: ${error.message}`, 'warning'); } } selectedSoftwareUrl = option.source; if (!selectedSoftwareUrl) { alert("Error: No download URL available"); await logCommunication('No download URL available for selected option', 'error'); return; } console.log("Download URL:", selectedSoftwareUrl); await logCommunication(`Downloading software from: ${selectedSoftwareUrl}`, 'sent'); // Log to v2/history before starting download (with customer data if provided) await logSoftwareInstallationToHistory(option, customerData); try { // Download the software file progressBar("10", "Downloading software...", "#04AA6D"); const response = await fetch(selectedSoftwareUrl, { method: "GET", headers: { "Authorization": "Bearer " + document.getElementById("servicetoken").textContent } }); if (!response.ok) { // Try to parse error message const errorText = await response.text(); let errorMessage = `Download failed: ${response.status}`; try { const errorJson = JSON.parse(errorText); errorMessage = errorJson.message || errorJson.error || errorMessage; } catch (e) { // Not JSON, use status text } console.error("Download failed:", response.status, errorText); await logCommunication(`Download failed: ${response.status} - ${errorText}`, 'error'); progressBar("0", "Error: " + errorMessage, "#ff6666"); alert("Download failed: " + errorMessage); return; } // Get the blob const blob = await response.blob(); progressBar("50", "Download complete, preparing installation...", "#04AA6D"); await logCommunication(`Software downloaded successfully, size: ${blob.size} bytes`, 'received'); // Create a blob URL for the downloaded file const blobUrl = URL.createObjectURL(blob); // Set global variables expected by upload.js window.firmwarelocation = blobUrl; window.upgraded_version = option.version || ""; // DEBUG MODE: Don't auto-trigger upload, let user manually test if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) { // Show upload section and button for manual testing document.getElementById("uploadSection").style.display = "block"; const uploadBtn = document.getElementById("uploadSoftware"); uploadBtn.style.display = "block"; uploadBtn.disabled = false; progressBar("100", "DEBUG MODE: Download complete. Click 'Install Software' button to test upload.js manually", "#ff9800"); console.log("=== DEBUG MODE: Manual Upload Test ==="); console.log("Firmware downloaded successfully"); console.log("Blob size:", blob.size, "bytes"); console.log("Blob URL:", blobUrl); console.log("window.firmwarelocation =", window.firmwarelocation); console.log("window.upgraded_version =", window.upgraded_version); console.log("Upload button is now visible and enabled"); console.log("Click the 'Install Software' button to test if upload.js can handle the file"); alert("DEBUG MODE: Download complete!\n\nBlob size: " + blob.size + " bytes\n\nClick the 'Install Software' button to test upload.js"); } else { // PRODUCTION MODE: Hide button and show installation in progress document.getElementById("uploadSection").style.display = "block"; const uploadBtn = document.getElementById("uploadSoftware"); uploadBtn.style.display = "none"; // Hide device version information during installation const softwareOptions = document.getElementById("softwareOptions"); if (softwareOptions) { softwareOptions.style.display = "none"; } // Create installation status indicator const installStatus = document.createElement("div"); installStatus.id = "installationStatus"; installStatus.style.cssText = ` text-align: center; padding: 20px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 8px; margin: 10px 0; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); `; installStatus.innerHTML = `

Installing Software...

Please keep your device connected and do not close this page

`; // Insert status before the hidden upload section document.getElementById("uploadSection").parentNode.insertBefore(installStatus, document.getElementById("uploadSection")); progressBar("60", "Starting automatic installation...", "#04AA6D"); // Enable the upload button and automatically click it setTimeout(() => { uploadBtn.disabled = false; // Start monitoring for completion if (typeof startUploadMonitoring === 'function') { startUploadMonitoring(); } uploadBtn.click(); }, 1000); } } catch (error) { await logCommunication(`Download error: ${error.message}`, 'error'); progressBar("0", "Error downloading software: " + error.message, "#ff6666"); alert("Error: " + error.message); } }