const STORAGE_KEY = "auth_jwt"; const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren const PARSE_TIMEOUT_MS = 15000; // 15 seconds const SCAN_TIMEOUT_MS = 45000; // 45 seconds (listing with _ipg=240 can be slow) const ACCOUNT_EXTENDED_TIMEOUT_MS = 15000; // 15 seconds per sub-scan const activeParseRequests = new Map(); // Map const activeScanRequests = new Map(); // Map; const activeExtendedParseRequests = new Map(); // Map /** Aktueller Scan-Fortschritt für GET_SCAN_PROGRESS (Polling durch Web-App) */ let currentScanProgress = null; // Messages from content script (der von der Web-App kommt) chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Auth messages vom Content Script if (msg?.type === "AUTH_JWT" && msg.jwt) { chrome.storage.local.set({ [STORAGE_KEY]: msg.jwt }).then(() => { sendResponse({ ok: true }); }); return true; // async } if (msg?.type === "AUTH_CLEARED") { chrome.storage.local.remove(STORAGE_KEY).then(() => { sendResponse({ ok: true }); }); return true; } // API calls vom Popup if (msg?.type === "GET_JWT") { getJwt().then(jwt => { sendResponse({ jwt }); }); return true; } if (msg?.type === "CALL_API" && msg.path) { callProtectedApi(msg.path, msg.payload).then(data => { sendResponse({ ok: true, data }); }).catch(err => { sendResponse({ ok: false, error: err.message }); }); return true; } // eBay Parsing Request (from Web App via content script or directly) if (msg?.action === "PARSE_URL" && msg.url) { handleParseRequest(msg.url, sendResponse); return true; // async } // eBay Parsing Response (from eBay content script) if (msg?.action === "PARSE_COMPLETE") { handleParseComplete(sender.tab?.id, msg.data); return true; } }); // Handle messages from external web apps (externally_connectable) chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => { if (msg?.action === "PARSE_URL" && msg.url) { handleParseRequest(msg.url, sendResponse); return true; // async } if (msg?.action === "SCAN_PRODUCTS" && msg.url && msg.accountId) { handleScanProductsRequest(msg.url, msg.accountId, sendResponse); return true; // async } if (msg?.action === "PARSE_ACCOUNT_EXTENDED" && msg.url) { handleParseAccountExtendedRequest(msg.url, sendResponse); return true; // async } if (msg?.action === "GET_SCAN_PROGRESS") { sendResponse(currentScanProgress ?? { percent: 0, phase: "idle", total: 0, current: 0, complete: false }); return false; } }); /** * Handles eBay URL parsing request * Creates a hidden tab, waits for load, sends parse message to content script */ async function handleParseRequest(url, sendResponse) { try { // Validate URL if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) { sendResponse({ ok: false, error: "Invalid eBay URL" }); return; } console.log("[BACKGROUND] Creating tab for parse request:", url); // Create hidden tab const tab = await chrome.tabs.create({ url: url, active: false }); const tabId = tab.id; console.log("[BACKGROUND] Tab created:", tabId); // Set up timeout const timeoutId = setTimeout(() => { cleanupParseRequest(tabId, null, { ok: false, error: "timeout" }); }, PARSE_TIMEOUT_MS); // Store request info activeParseRequests.set(tabId, { timeout: timeoutId, sendResponse: sendResponse }); // Wait for tab to load, then send parse message const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => { if (updatedTabId !== tabId) return; // Tab is fully loaded if (changeInfo.status === 'complete' && updatedTab.url) { chrome.tabs.onUpdated.removeListener(checkTabLoaded); console.log("[BACKGROUND] Tab loaded, sending parse message:", updatedTab.url); // Wait longer for content script to auto-load (if it does) // Content scripts from manifest.json might need more time in hidden tabs setTimeout(() => { sendParseMessageWithRetry(tabId, 0); }, 2000); // 2 seconds delay - give content script time to auto-load } }; chrome.tabs.onUpdated.addListener(checkTabLoaded); } catch (error) { console.error("Error in handleParseRequest:", error); sendResponse({ ok: false, error: error.message || "Unknown error" }); } } /** * Checks if a URL is an eBay URL * @param {string} url - URL to check * @returns {boolean} - true if URL is an eBay URL */ function isEbayUrl(url) { if (!url) return false; const urlLower = url.toLowerCase(); return urlLower.includes('ebay.'); } /** * Injects the eBay content script manually if it's not already loaded * @param {number} tabId - Tab ID * @returns {Promise} - true if injection successful or already loaded */ async function ensureContentScriptInjected(tabId) { try { // First, check if tab URL is an eBay URL const tab = await chrome.tabs.get(tabId); if (!isEbayUrl(tab.url)) { console.log("[BACKGROUND] Tab is not an eBay URL:", tab.url); return false; } // Check if content script is already loaded by sending a ping try { await chrome.tabs.sendMessage(tabId, { action: "PING" }); console.log("[BACKGROUND] Content script already loaded"); return true; } catch (pingError) { // Content script not loaded, try to inject it manually console.log("[BACKGROUND] Content script not found, injecting manually..."); try { // Get tab info to check frames const tabInfo = await chrome.tabs.get(tabId); // Inject full ebay-content-script.js so PING, PARSE_EBAY and PARSE_PRODUCT_LIST are all handled. // The previous inline injection only handled PING+PARSE_EBAY; SCAN sends PARSE_PRODUCT_LIST. try { await chrome.scripting.executeScript({ target: { tabId: tabId, frameIds: [0] }, files: ["ebay-content-script.js"] }); console.log("[BACKGROUND] ebay-content-script.js injected successfully"); } catch (injectError) { console.error("[BACKGROUND] Failed to inject ebay-content-script:", injectError); throw injectError; } // Also try injecting a simple test script to verify injection works try { await chrome.scripting.executeScript({ target: { tabId: tabId, frameIds: [0] }, func: () => { console.log("[TEST-INJECTION] Test script executed in main frame"); } }); } catch (testError) { } // Wait longer for the script to fully initialize and register message listeners // Retry PING to verify the script is ready // Also try sending to frameId 0 explicitly for (let pingAttempt = 0; pingAttempt < 10; pingAttempt++) { await new Promise(resolve => setTimeout(resolve, 300)); // 300ms between pings try { // Try sending to main frame explicitly try { await chrome.tabs.sendMessage(tabId, { action: "PING" }, { frameId: 0 }); } catch (frameErr) { // Fallback: try without frameId await chrome.tabs.sendMessage(tabId, { action: "PING" }); } console.log("[BACKGROUND] Content script ready after injection"); return true; } catch (pingErr) { // Continue to next attempt } } // Even if PING failed, return true - the script might still work console.warn("[BACKGROUND] Content script injected but PING not responding"); return true; } catch (injectError) { console.error("[BACKGROUND] Failed to inject content script:", injectError); return false; } } } catch (error) { console.error("[BACKGROUND] Error checking/injecting content script:", error); return false; } } /** * Sends parse message to content script with retry mechanism * @param {number} tabId - Tab ID * @param {number} attempt - Current attempt number (0-based) */ async function sendParseMessageWithRetry(tabId, attempt) { const maxAttempts = 3; const retryDelay = 500; // 500ms between retries try { console.log(`[BACKGROUND] Sending parse message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId); // On first attempt, ensure content script is injected if (attempt === 0) { const injected = await ensureContentScriptInjected(tabId); if (!injected) { throw new Error("Could not inject content script"); } } // Check if content script is injected by trying to send a ping // If this fails, the content script might not be loaded yet // Try sending to main frame explicitly first let response; try { response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }, { frameId: 0 }); } catch (frameErr) { // Fallback: try without frameId response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }); } console.log("[BACKGROUND] Parse response received:", response); if (response && response.ok && response.data) { handleParseComplete(tabId, response.data); } else { cleanupParseRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed" }); } } catch (err) { // Check if error is due to content script not being ready const runtimeError = chrome.runtime.lastError?.message || ""; const isContentScriptError = err.message?.includes("Could not establish connection") || err.message?.includes("Receiving end does not exist") || err.message?.includes("Could not inject content script") || runtimeError.includes("Receiving end does not exist") || runtimeError.includes("Could not establish connection"); console.log(`[BACKGROUND] Error sending parse message (attempt ${attempt + 1}):`, { error: err.message, runtimeError: runtimeError, isContentScriptError: isContentScriptError }); if (isContentScriptError && attempt < maxAttempts) { // Content script not ready yet, retry after delay console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`); setTimeout(() => { sendParseMessageWithRetry(tabId, attempt + 1); }, retryDelay); } else { // Max retries reached or different error const errorMessage = isContentScriptError ? "Content script konnte nicht geladen werden. Bitte Extension neu laden." : (err.message || runtimeError || "Content script error"); cleanupParseRequest(tabId, null, { ok: false, error: errorMessage }); } } } /** * Handles parse complete response from content script */ function handleParseComplete(tabId, data) { cleanupParseRequest(tabId, data, null); } /** * Cleans up parse request: closes tab, clears timeout, sends response */ async function cleanupParseRequest(tabId, data, error) { const request = activeParseRequests.get(tabId); if (!request) return; // Clear timeout if (request.timeout) { clearTimeout(request.timeout); } // Remove from active requests activeParseRequests.delete(tabId); // Close tab try { await chrome.tabs.remove(tabId); } catch (err) { // Tab might already be closed console.warn("Could not close tab:", err); } // Send response if (request.sendResponse) { if (error) { request.sendResponse(error); } else if (data) { request.sendResponse({ ok: true, data: data }); } else { request.sendResponse({ ok: false, error: "Unknown error" }); } } } /** * Transform account URL (e.g. /str/topmons) to listing URL with 240 items per page. * Example: https://www.ebay.de/str/topmons?... → https://www.ebay.de/sch/i.html?_ssn=topmons&store_name=topmons&_ipg=240 */ function transformAccountUrlTo240ItemsUrl(accountUrl) { try { const url = new URL(accountUrl); const pathname = url.pathname; const strMatch = pathname.match(/\/str\/([^\/\?]+)/); if (!strMatch || !strMatch[1]) { throw new Error("Could not extract account name from URL (expected /str/{name})"); } const accountName = strMatch[1]; const baseUrl = `${url.protocol}//${url.hostname}`; const listingUrl = `${baseUrl}/sch/i.html?_ssn=${encodeURIComponent(accountName)}&store_name=${encodeURIComponent(accountName)}&_ipg=240`; return listingUrl; } catch (e) { throw new Error(`Failed to transform account URL: ${e.message}`); } } /** * Handles eBay product scan request * Transforms account URL to listing URL with _ipg=240, creates hidden tab, parses, then loads each item in separate tab. */ async function handleScanProductsRequest(url, accountId, sendResponse) { try { // Validate URL if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) { sendResponse({ ok: false, error: "Invalid eBay URL" }); return; } let targetUrl = url; try { targetUrl = transformAccountUrlTo240ItemsUrl(url); console.log("[BACKGROUND] Scan using listing URL (240 items):", targetUrl); } catch (transformErr) { console.warn("[BACKGROUND] URL transform failed, using original URL:", transformErr.message); // Fallback: use original URL (e.g. already /sch/ or non-/str/) } // Create hidden tab const tab = await chrome.tabs.create({ url: targetUrl, active: false }); const tabId = tab.id; // Set up timeout const timeoutId = setTimeout(() => { cleanupScanRequest(tabId, null, { ok: false, error: "timeout" }); }, SCAN_TIMEOUT_MS); // Store request info activeScanRequests.set(tabId, { timeout: timeoutId, sendResponse: sendResponse }); currentScanProgress = { percent: 0, phase: "listing", total: 0, current: 0, complete: false }; // Wait for tab to load, then send parse message const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => { if (updatedTabId !== tabId) return; // Tab is fully loaded if (changeInfo.status === 'complete' && updatedTab.url) { chrome.tabs.onUpdated.removeListener(checkTabLoaded); // Small delay to ensure DOM is ready setTimeout(() => { sendScanMessageWithRetry(tabId, 0); }, 1000); // 1 second delay for DOM ready } }; chrome.tabs.onUpdated.addListener(checkTabLoaded); } catch (error) { console.error("Error in handleScanProductsRequest:", error); sendResponse({ ok: false, error: error.message || "Unknown error" }); } } /** * Sends scan message to content script with retry mechanism * @param {number} tabId - Tab ID * @param {number} attempt - Current attempt number (0-based) */ async function sendScanMessageWithRetry(tabId, attempt) { const maxAttempts = 3; const retryDelay = 500; // 500ms between retries try { console.log(`[BACKGROUND] Sending scan message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId); // On first attempt, ensure content script is injected if (attempt === 0) { const injected = await ensureContentScriptInjected(tabId); if (!injected) { throw new Error("Could not inject content script"); } } const response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" }); if (response && response.ok) { // Prüfe ob items vorhanden und nicht leer const items = response.items || response.data?.items || []; const meta = response.meta || response.data?.meta || {}; // Log meta für Debugging console.log("[SCAN meta]", meta); if (items.length === 0) { // Leere items: behandele als Fehler mit meta cleanupScanRequest(tabId, null, { ok: false, error: "empty_items", meta: meta }); } else { if (currentScanProgress) { currentScanProgress.phase = "details"; currentScanProgress.total = items.length; currentScanProgress.percent = 10; } await handleScanComplete(tabId, { items, meta }); } } else { // Fehler: sende error + meta const meta = response?.meta || {}; console.log("[SCAN meta]", meta); cleanupScanRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed", meta: meta }); } } catch (err) { // Check if error is due to content script not being ready const runtimeError = chrome.runtime.lastError?.message || ""; const isContentScriptError = err.message?.includes("Could not establish connection") || err.message?.includes("Receiving end does not exist") || err.message?.includes("Could not inject content script") || runtimeError.includes("Receiving end does not exist") || runtimeError.includes("Could not establish connection"); console.log(`[BACKGROUND] Error sending scan message (attempt ${attempt + 1}):`, { error: err.message, runtimeError: runtimeError, isContentScriptError: isContentScriptError }); if (isContentScriptError && attempt < maxAttempts) { // Content script not ready yet, retry after delay console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`); setTimeout(() => { sendScanMessageWithRetry(tabId, attempt + 1); }, retryDelay); } else { // Max retries reached or different error const errorMessage = isContentScriptError ? "Content script konnte nicht geladen werden. Bitte Extension neu laden." : (err.message || runtimeError || "Content script error"); cleanupScanRequest(tabId, null, { ok: false, error: errorMessage, meta: { pageType: "unknown", finalUrl: "", attempts: attempt + 1, reason: "content_script_error" } }); } } } const ITEM_TAB_LOAD_TIMEOUT_MS = 3000; /** * Opens each item URL in a separate background tab, waits for load, parses detail page * (title, price, currency, category, condition), merges into item, then closes tab. */ async function loadAndParseEachItemTab(items) { if (!Array.isArray(items) || items.length === 0) return; const total = items.length; for (let i = 0; i < total; i++) { const item = items[i]; if (!item || !item.url) continue; try { const tab = await chrome.tabs.create({ url: item.url, active: false }); await new Promise((resolve) => { const listener = (tabId, changeInfo) => { if (tabId === tab.id && changeInfo.status === "complete") { chrome.tabs.onUpdated.removeListener(listener); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, ITEM_TAB_LOAD_TIMEOUT_MS); }); const injected = await ensureContentScriptInjected(tab.id); if (!injected) { console.warn(`[BACKGROUND] Content script not ready in item tab ${i + 1}, skipping detail parse`); } try { const response = injected ? await chrome.tabs.sendMessage(tab.id, { action: "PARSE_ITEM_DETAIL" }) : null; if (response?.ok && response?.data) { const d = response.data; if (d.title != null && d.title !== "") item.title = d.title; if (d.price != null) item.price = d.price; if (d.currency != null && d.currency !== "") item.currency = d.currency; if (d.category != null && d.category !== "") item.category = d.category; if (d.condition != null && d.condition !== "") item.condition = d.condition; if (d.quantityAvailable != null) item.quantityAvailable = d.quantityAvailable; if (d.quantitySold != null) item.quantitySold = d.quantitySold; if (d.watchCount != null) item.watchCount = d.watchCount; if (d.inCartsCount != null) item.inCartsCount = d.inCartsCount; } } catch (parseErr) { console.warn(`[BACKGROUND] PARSE_ITEM_DETAIL failed for ${item.url}:`, parseErr); } if (currentScanProgress && total > 0) { currentScanProgress.current = i + 1; currentScanProgress.percent = Math.min(99, 10 + Math.round((90 * (i + 1)) / total)); } try { await chrome.tabs.remove(tab.id); } catch (removeErr) { console.warn(`[BACKGROUND] Could not close item tab:`, removeErr); } } catch (e) { console.warn(`[BACKGROUND] Failed to load/parse item tab ${i + 1}/${total}:`, item.url, e); } } } /** * Handles scan complete response from content script */ async function handleScanComplete(tabId, data) { const request = activeScanRequests.get(tabId); if (request && request.timeout) { clearTimeout(request.timeout); request.timeout = null; } const items = data?.items || []; if (items.length > 0) { await loadAndParseEachItemTab(items); } if (currentScanProgress) { currentScanProgress.percent = 100; currentScanProgress.complete = true; currentScanProgress.current = items.length; } await cleanupScanRequest(tabId, data, null); } /** * Cleans up scan request: closes tab, clears timeout, sends response */ async function cleanupScanRequest(tabId, data, error) { const request = activeScanRequests.get(tabId); if (!request) return; // Clear timeout if (request.timeout) { clearTimeout(request.timeout); } // Remove from active requests activeScanRequests.delete(tabId); currentScanProgress = null; // Close tab (always, even on error) try { await chrome.tabs.remove(tabId); } catch (err) { // Tab might already be closed console.warn("Could not close tab:", err); } // Send response if (request.sendResponse) { if (error) { // error kann bereits meta enthalten request.sendResponse(error); } else if (data) { // data kann items + meta enthalten request.sendResponse({ ok: true, data: data }); } else { request.sendResponse({ ok: false, error: "Unknown error" }); } } } /** * Handles extended account parsing request * Visits multiple URLs (base, feedback, about, store) and collects partial results */ async function handleParseAccountExtendedRequest(url, sendResponse) { try { // Validate URL if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) { sendResponse({ ok: false, error: "Invalid eBay URL" }); return; } console.log("[BACKGROUND] Starting extended account parse for:", url); const requestId = `extended_${Date.now()}_${Math.random()}`; const results = { base: { ok: false, data: null, error: null }, feedback: { ok: false, data: null, error: null }, about: { ok: false, data: null, error: null }, store: { ok: false, data: null, error: null } }; activeExtendedParseRequests.set(requestId, { sendResponse: sendResponse, results: results }); // Step 1: Parse base URL (like normal parse) try { const baseData = await parseSingleTab(url, "PARSE_EBAY", ACCOUNT_EXTENDED_TIMEOUT_MS); results.base = { ok: true, data: baseData, error: null }; } catch (error) { console.warn("[BACKGROUND] Base parse failed:", error); results.base = { ok: false, data: null, error: error.message || "Base parse failed" }; } // Step 2: Parse feedback tab try { const feedbackUrl = appendQueryParam(url, "_tab=feedback"); const feedbackData = await parseSingleTab(feedbackUrl, "PARSE_FEEDBACK", ACCOUNT_EXTENDED_TIMEOUT_MS); results.feedback = { ok: true, data: feedbackData, error: null }; } catch (error) { console.warn("[BACKGROUND] Feedback parse failed:", error); results.feedback = { ok: false, data: null, error: error.message || "Feedback parse failed" }; } // Step 3: Parse about tab try { const aboutUrl = appendQueryParam(url, "_tab=about"); const aboutData = await parseSingleTab(aboutUrl, "PARSE_ABOUT", ACCOUNT_EXTENDED_TIMEOUT_MS); results.about = { ok: true, data: aboutData, error: null }; } catch (error) { console.warn("[BACKGROUND] About parse failed:", error); results.about = { ok: false, data: null, error: error.message || "About parse failed" }; } // Step 4: Find and parse store URL try { let storeUrl = null; // First, try to find store link on base page if (results.base.ok) { try { // Re-open base page tab to search for store link const baseTab = await chrome.tabs.create({ url: url, active: false }); await new Promise((resolve) => { const checkLoaded = (tabId, changeInfo) => { if (tabId === baseTab.id && changeInfo.status === 'complete') { chrome.tabs.onUpdated.removeListener(checkLoaded); setTimeout(resolve, 2000); } }; chrome.tabs.onUpdated.addListener(checkLoaded); setTimeout(resolve, 5000); // Max wait }); const injected = await ensureContentScriptInjected(baseTab.id); if (injected) { try { const linkResponse = await chrome.tabs.sendMessage(baseTab.id, { action: "FIND_STORE_LINK" }); if (linkResponse && linkResponse.ok && linkResponse.data && linkResponse.data.storeUrl) { storeUrl = linkResponse.data.storeUrl; } } catch (e) { console.warn("[BACKGROUND] Could not find store link via content script:", e); } } await chrome.tabs.remove(baseTab.id); } catch (e) { console.warn("[BACKGROUND] Error searching for store link:", e); } } // Fallback: heuristical derivation if (!storeUrl) { const sellerId = results.base.data?.sellerId || null; storeUrl = deriveStoreUrl(url, sellerId); } if (storeUrl) { const storeData = await parseSingleTab(storeUrl, "PARSE_STORE", ACCOUNT_EXTENDED_TIMEOUT_MS); results.store = { ok: true, data: storeData, error: null }; } else { results.store = { ok: false, data: null, error: "Could not determine store URL" }; } } catch (error) { console.warn("[BACKGROUND] Store parse failed:", error); results.store = { ok: false, data: null, error: error.message || "Store parse failed" }; } // Combine all results const combinedData = { // Base data (always present if base.ok) sellerId: results.base.data?.sellerId || "", shopName: results.base.data?.shopName || "", market: results.base.data?.market || "US", status: results.base.data?.status || "unknown", stats: results.base.data?.stats || {}, // Extended data (can be null) responseTimeHours: results.about.data?.responseTimeHours || null, followers: results.store.data?.followers || null, feedbackTotal: results.feedback.data?.feedbackTotal || null, feedback12mPositive: results.feedback.data?.feedback12mPositive || null, feedback12mNeutral: results.feedback.data?.feedback12mNeutral || null, feedback12mNegative: results.feedback.data?.feedback12mNegative || null, // Partial results status partialResults: { base: { ok: results.base.ok, error: results.base.error }, feedback: { ok: results.feedback.ok, error: results.feedback.error }, about: { ok: results.about.ok, error: results.about.error }, store: { ok: results.store.ok, error: results.store.error } } }; activeExtendedParseRequests.delete(requestId); sendResponse({ ok: true, data: combinedData }); } catch (error) { console.error("Error in handleParseAccountExtendedRequest:", error); sendResponse({ ok: false, error: error.message || "Unknown error" }); } } /** * Parses a single tab with given action and timeout * @param {string} url - URL to parse * @param {string} action - Action to send to content script (PARSE_EBAY, PARSE_FEEDBACK, etc.) * @param {number} timeoutMs - Timeout in milliseconds * @returns {Promise} Parsed data */ async function parseSingleTab(url, action, timeoutMs) { return new Promise(async (resolve, reject) => { let tabId = null; let timeoutId = null; try { // Create hidden tab const tab = await chrome.tabs.create({ url: url, active: false }); tabId = tab.id; // Set up timeout timeoutId = setTimeout(() => { cleanupSingleTab(tabId); reject(new Error("timeout")); }, timeoutMs); // Wait for tab to load const checkTabLoaded = (updatedTabId, changeInfo) => { if (updatedTabId !== tabId) return; if (changeInfo.status === 'complete') { chrome.tabs.onUpdated.removeListener(checkTabLoaded); setTimeout(async () => { try { const injected = await ensureContentScriptInjected(tabId); if (!injected) { cleanupSingleTab(tabId); reject(new Error("Could not inject content script")); return; } const response = await chrome.tabs.sendMessage(tabId, { action: action }); cleanupSingleTab(tabId); if (response && response.ok && response.data) { resolve(response.data); } else { reject(new Error(response?.error || "Parsing failed")); } } catch (err) { cleanupSingleTab(tabId); reject(err); } }, 2000); // 2 second delay for DOM ready } }; chrome.tabs.onUpdated.addListener(checkTabLoaded); } catch (error) { if (timeoutId) clearTimeout(timeoutId); if (tabId) cleanupSingleTab(tabId); reject(error); } }); } /** * Cleans up a single tab */ async function cleanupSingleTab(tabId) { try { await chrome.tabs.remove(tabId); } catch (err) { // Tab might already be closed console.warn("[BACKGROUND] Could not close tab:", err); } } /** * Appends a query parameter to a URL * @param {string} url - Base URL * @param {string} param - Query parameter (e.g. "_tab=feedback") * @returns {string} URL with appended parameter */ function appendQueryParam(url, param) { try { const urlObj = new URL(url); urlObj.search += (urlObj.search ? '&' : '?') + param; return urlObj.href; } catch (e) { // Fallback: simple string append return url + (url.includes('?') ? '&' : '?') + param; } } /** * Derives store URL from account URL * @param {string} accountUrl - Account URL * @param {string|null} sellerId - Seller ID if available * @returns {string|null} Store URL or null */ function deriveStoreUrl(accountUrl, sellerId) { try { const urlObj = new URL(accountUrl); const pathname = urlObj.pathname; // Try to extract seller ID from URL if not provided let id = sellerId; if (!id) { const usrMatch = pathname.match(/\/usr\/([^\/\?]+)/); if (usrMatch && usrMatch[1]) { id = usrMatch[1]; } else { const strMatch = pathname.match(/\/str\/([^\/\?]+)/); if (strMatch && strMatch[1]) { id = strMatch[1]; } } } if (id) { return `${urlObj.protocol}//${urlObj.hostname}/str/${encodeURIComponent(id)}`; } return null; } catch (e) { console.warn("[BACKGROUND] Failed to derive store URL:", e); return null; } } export async function getJwt() { const data = await chrome.storage.local.get(STORAGE_KEY); return data[STORAGE_KEY] || ""; } export async function callProtectedApi(path, payload) { const jwt = await getJwt(); if (!jwt) throw new Error("Not authed"); const res = await fetch(`${BACKEND_URL}${path}`, { method: "POST", headers: { "content-type": "application/json", "authorization": `Bearer ${jwt}` }, body: JSON.stringify(payload || {}) }); if (!res.ok) throw new Error(`API error: ${res.status}`); return await res.json(); }