1920 lines
83 KiB
JavaScript
1920 lines
83 KiB
JavaScript
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 `<option value="">${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'}</option>`;
|
|
}
|
|
|
|
// Sort countries alphabetically
|
|
const sortedCountries = Object.values(COUNTRIES).sort((a, b) => {
|
|
return a.country.localeCompare(b.country);
|
|
});
|
|
|
|
let options = '<option value="">Select country</option>';
|
|
sortedCountries.forEach(data => {
|
|
const selected = (selectedCountry === data.country) ? 'selected' : '';
|
|
options += `<option value="${data.country}" ${selected}>${data.country}</option>`;
|
|
});
|
|
|
|
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 = `
|
|
<i class="fa-solid fa-exclamation-triangle" style="font-size: 24px;"></i>
|
|
<div style="flex: 1;">
|
|
<strong style="display: block; margin-bottom: 5px; font-size: 16px;">Browser Not Supported</strong>
|
|
<p style="margin: 0; font-size: 14px; opacity: 0.95;">
|
|
Please use <strong>Chrome</strong>, <strong>Edge</strong>, or <strong>Opera</strong> to access device connectivity features.
|
|
</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `<strong style="color: #ff9800;">DEBUG MODE - Simulated Device Data:</strong><br>SN=${deviceSerialNumber}<br>FW=${deviceVersion}<br>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 = `<i class="fa-solid fa-microchip" style="color: ${isFree ? '#04AA6D' : '#FF6B35'}; margin-right: 8px;"></i>${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 = `<i class="fa-solid fa-code-branch" style="color: #999;"></i> <span style="font-weight: 500;">Version:</span> <strong>${option.version || "N/A"}</strong>`;
|
|
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 = '<i class="fa-solid fa-handshake" style="margin-right: 8px;"></i>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 += `<div style="font-weight: 600; margin-bottom: 8px; color: #1565c0;"><i class="fa-solid fa-building" style="margin-right: 8px; width: 16px;"></i>${dealerInfo.name}</div>`;
|
|
}
|
|
if (dealerInfo.address || dealerInfo.city || dealerInfo.postalcode || dealerInfo.country) {
|
|
dealerHtml += `<div style="margin-bottom: 6px; color: #666; display: flex; align-items: flex-start;"><i class="fa-solid fa-location-dot" style="margin-right: 8px; width: 16px; color: #999; margin-top: 2px;"></i><div>`;
|
|
if (dealerInfo.address) {
|
|
dealerHtml += `<div>${dealerInfo.address}</div>`;
|
|
}
|
|
if (dealerInfo.postalcode || dealerInfo.city) {
|
|
dealerHtml += `<div>${[dealerInfo.postalcode, dealerInfo.city].filter(Boolean).join(' ')}</div>`;
|
|
}
|
|
if (dealerInfo.country) {
|
|
dealerHtml += `<div>${dealerInfo.country}</div>`;
|
|
}
|
|
dealerHtml += `</div></div>`;
|
|
}
|
|
if (dealerInfo.email) {
|
|
dealerHtml += `<div style="margin-bottom: 6px;"><i class="fa-solid fa-envelope" style="margin-right: 8px; width: 16px; color: #999;"></i><a href="mailto:${dealerInfo.email}" style="color: #1565c0; text-decoration: none;">${dealerInfo.email}</a></div>`;
|
|
}
|
|
if (dealerInfo.phone) {
|
|
dealerHtml += `<div><i class="fa-solid fa-phone" style="margin-right: 8px; width: 16px; color: #999;"></i><a href="tel:${dealerInfo.phone}" style="color: #1565c0; text-decoration: none;">${dealerInfo.phone}</a></div>`;
|
|
}
|
|
|
|
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 = '<i class="fa-solid fa-check-circle"></i> INSTALLED';
|
|
} else {
|
|
priceText.innerHTML = isFree
|
|
? 'Free'
|
|
: `${option.currency || "€"} ${price.toFixed(2)} <small style="font-size: 12px; font-weight: 400; color: #888;">(excl. VAT)</small>`;
|
|
}
|
|
|
|
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 = '<i class="fa-solid fa-check"></i> Currently Installed';
|
|
actionBtn.disabled = true;
|
|
} else if (isFree) {
|
|
actionBtn.innerHTML = '<i class="fa-solid fa-download"></i>';
|
|
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 = '<i class="fa-solid fa-shopping-cart"></i>';
|
|
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 = `
|
|
<div style="padding: 30px; border-bottom: 2px solid #e0e0e0;">
|
|
<h3 style="margin: 0; color: #333; font-size: 24px;">${typeof TRANS_USER_INFO_REQUIRED !== 'undefined' ? TRANS_USER_INFO_REQUIRED : 'User Information Required'}</h3>
|
|
<p style="margin: 10px 0 0 0; color: #666; font-size: 14px;">${typeof TRANS_USER_INFO_DESCRIPTION !== 'undefined' ? TRANS_USER_INFO_DESCRIPTION : 'Please provide your information to continue with software updates'}</p>
|
|
</div>
|
|
<div style="padding: 30px;">
|
|
<form id="userInfoForm" style="display: flex; flex-direction: column; gap: 15px;">
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
|
<input type="text" name="name" id="userInfoName" required style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
|
<input type="email" name="email" id="userInfoEmail" required style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
|
<input type="text" name="address" id="userInfoAddress" required placeholder="Street and number" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; margin-bottom: 10px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
<input type="text" name="city" id="userInfoCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; margin-bottom: 10px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<input type="text" name="postal" id="userInfoPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
<select name="country" id="userInfoCountry" required style="padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
|
${generateCountryOptions()}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
|
<button type="button" onclick="location.reload()" style="padding: 15px 20px; background: #6c757d; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='#5a6268'" onmouseout="this.style.background='#6c757d'">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
<button type="submit" style="flex: 1; padding: 15px; background: linear-gradient(135deg, #04AA6D 0%, #038f5a 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(4,170,109,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
|
${typeof TRANS_CONTINUE !== 'undefined' ? TRANS_CONTINUE : 'Continue'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="padding: 30px; border-bottom: 2px solid #e0e0e0;">
|
|
<h3 style="margin: 0; color: #333; font-size: 24px;">${typeof TRANS_HW_WARNING_TITLE !== 'undefined' ? TRANS_HW_WARNING_TITLE : 'Hardware Compatibility Notice'}</h3>
|
|
<p style="margin: 10px 0 0 0; color: #666; font-size: 14px;">${typeof TRANS_HW_WARNING_SUBTITLE !== 'undefined' ? TRANS_HW_WARNING_SUBTITLE : 'Please read the following information carefully'}</p>
|
|
</div>
|
|
<div style="padding: 30px;">
|
|
<div style="margin-bottom: 20px; padding: 20px; background: #fff3cd; border-left: 4px solid #ff9800; border-radius: 4px;">
|
|
<div style="display: flex; align-items: flex-start; gap: 15px;">
|
|
<i class="fa-solid fa-exclamation-triangle" style="font-size: 32px; color: #ff9800; margin-top: 5px;"></i>
|
|
<div>
|
|
<p style="margin: 0 0 10px 0; color: #333; font-size: 14px; font-weight: 600;">
|
|
${typeof TRANS_HW_WARNING_DETECTED !== 'undefined' ? TRANS_HW_WARNING_DETECTED.replace('{hw_version}', deviceHwVersion) : `Hardware version ${deviceHwVersion} detected`}
|
|
</p>
|
|
<p style="margin: 0; color: #666; font-size: 14px; line-height: 1.6;">
|
|
${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.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="hwWarningContinue" style="width: 100%; padding: 15px; background: linear-gradient(135deg, #04AA6D 0%, #038f5a 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(4,170,109,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
|
${typeof TRANS_CONTINUE !== 'undefined' ? TRANS_CONTINUE : 'Continue'}
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="padding: 25px; border-bottom: 1px solid #e0e0e0;">
|
|
<h3 style="margin: 0; color: #333;"><i class="fa-solid fa-download"></i> Confirm Software Installation</h3>
|
|
</div>
|
|
<div style="padding: 25px;">
|
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
|
<h4 style="margin: 0 0 10px 0; color: #333;">${option.name || "Software Update"}</h4>
|
|
<p style="margin: 0 0 5px 0; color: #666;">Version: <strong>${option.version || "N/A"}</strong></p>
|
|
<p style="margin: 0 0 15px 0; color: #666;">${option.description || ""}</p>
|
|
</div>
|
|
|
|
<form id="freeInstallForm" style="display: flex; flex-direction: column; gap: 15px;">
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
|
<input type="text" name="name" id="freeInstallName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
|
<input type="email" name="email" id="freeInstallEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
|
<input type="text" name="address" id="freeInstallAddress" required placeholder="Street and number" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
|
<input type="text" name="city" id="freeInstallCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<input type="text" name="postal" id="freeInstallPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
<select name="country" id="freeInstallCountry" required style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
${generateCountryOptions()}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
|
<button type="button" id="cancelFreeInstall" style="flex: 1; padding: 12px; background: #6c757d; color: white; border: none; border-radius: 6px; font-size: 20px; font-weight: 600; cursor: pointer;">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
<button type="submit" style="flex: 1; padding: 12px; background: #04AA6D; color: white; border: none; border-radius: 6px; font-size: 20px; font-weight: 600; cursor: pointer;">
|
|
<i class="fa-solid fa-check"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
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 '<ul style="margin: 0; padding-left: 20px; color: #666; font-size: 13px; line-height: 1.6;">' +
|
|
lines.map(line => `<li>${line}</li>`).join('') +
|
|
'</ul>';
|
|
};
|
|
|
|
// 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 = `
|
|
<div style="padding: 25px; border-bottom: 1px solid #e0e0e0;">
|
|
<h3 style="margin: 0; color: #333;"><i class="fa-solid fa-shopping-cart"></i> Purchase Software Upgrade</h3>
|
|
</div>
|
|
<div style="padding: 25px;">
|
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
|
<h4 style="margin: 0 0 10px 0; color: #333;">${option.name || "Software Update"}</h4>
|
|
<p style="margin: 0 0 5px 0; color: #666;">Version: <strong>${option.version || "N/A"}</strong></p>
|
|
<div style="margin: 0 0 15px 0;">${formatDescription(option.description)}</div>
|
|
<div id="priceDisplay" style="font-size: 14px; color: #666;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Price (excl. VAT):</span>
|
|
<span style="font-weight: 600;">${currency} ${price.toFixed(2)}</span>
|
|
</div>
|
|
<div id="taxDisplay" style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>VAT:</span>
|
|
<span style="font-weight: 600;">-</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding-top: 10px; border-top: 2px solid #ddd; margin-top: 10px;">
|
|
<span style="font-weight: bold;">Total:</span>
|
|
<span id="totalDisplay" style="font-size: 24px; font-weight: bold; color: #04AA6D;">${currency} ${price.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="paymentForm" style="display: flex; flex-direction: column; gap: 15px;">
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
|
<input type="text" name="name" id="paymentName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
|
<input type="email" name="email" id="paymentEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
|
<input type="text" name="address" id="paymentAddress" required placeholder="Street and number" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
|
<input type="text" name="city" id="paymentCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<input type="text" name="postal" id="paymentPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
<select name="country" id="paymentCountry" required style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
${generateCountryOptions()}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">VAT Number</label>
|
|
<input type="text" name="vat_number" id="paymentVatNumber" placeholder="Optional" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label>
|
|
<select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
<option value="">Select payment method</option>
|
|
${typeof MOLLIE_ENABLED !== 'undefined' && MOLLIE_ENABLED ? '<option value="1">Credit Card</option>' : ''}
|
|
${typeof PAYPAL_ENABLED !== 'undefined' && PAYPAL_ENABLED ? '<option value="3">PayPal</option>' : ''}
|
|
${typeof PAY_ON_DELIVERY_ENABLED !== 'undefined' && PAY_ON_DELIVERY_ENABLED ? '<option value="bank_transfer">Bank Transfer</option>' : ''}
|
|
</select>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
|
<button type="button" id="cancelPayment" style="flex: 1; padding: 12px; background: #6c757d; color: white; border: none; border-radius: 6px; font-size: 20px; font-weight: 600; cursor: pointer;">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
<button type="submit" style="flex: 1; padding: 12px; background: #04AA6D; color: white; border: none; border-radius: 6px; font-size: 20px; font-weight: 600; cursor: pointer;">
|
|
<i class="fa-solid fa-credit-card"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
modal.appendChild(modalContent);
|
|
document.body.appendChild(modal);
|
|
|
|
// Function to calculate and update tax
|
|
function updateTaxDisplay() {
|
|
const selectedCountry = document.getElementById("paymentCountry").value;
|
|
let taxRate = 0;
|
|
|
|
if (selectedCountry && typeof COUNTRIES !== 'undefined' && COUNTRIES) {
|
|
const countryData = Object.values(COUNTRIES).find(c => c.country === selectedCountry);
|
|
if (countryData) {
|
|
taxRate = parseFloat(countryData.taxes) || 0;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<span>VAT (${taxRate}%):</span>
|
|
<span style="font-weight: 600;">${currency} ${taxAmount.toFixed(2)}</span>
|
|
`;
|
|
} else {
|
|
taxDisplay.innerHTML = `
|
|
<span>VAT:</span>
|
|
<span style="font-weight: 600;">-</span>
|
|
`;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Add event listener to country select to update tax
|
|
document.getElementById("paymentCountry").addEventListener('change', updateTaxDisplay);
|
|
|
|
// 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 = `
|
|
<i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: #04AA6D; margin-bottom: 10px;"></i>
|
|
<p style="margin: 0; font-size: 18px; font-weight: 600; color: #333;">Installing Software...</p>
|
|
<p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">Please keep your device connected and do not close this page</p>
|
|
`;
|
|
|
|
// 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);
|
|
}
|
|
}
|