Last active
December 5, 2025 16:58
-
-
Save dzid26/d81fbaf22e6964d936b5ea44359d97f1 to your computer and use it in GitHub Desktop.
revert to simple download
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name AliExpress Invoice Downloader | |
| // @namespace http://tampermonkey.net/ | |
| // @version 3.4 | |
| // @description Single-click to download. Hover to see URL. Right-click to copy link. | |
| // @author Antigravity/dzid26 | |
| // @match https://www.aliexpress.com/p/order/* | |
| // @match https://www.aliexpress.us/p/order/* | |
| // @match https://www.aliexpress.ru/p/order/* | |
| // @grant GM_xmlhttpRequest | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| // CSS: Updated for <a> tag (no underline, inline-block) | |
| const css = `.ali-invoice-btn{display:inline-block;text-decoration:none!important;margin-left:10px;padding:2px 8px;background:#ff4747;color:#fff!important;border-radius:4px;font-size:12px;font-weight:700;line-height:1.5;cursor:pointer;transition:background .2s}.ali-invoice-btn:hover{background:#d63030}.ali-invoice-btn.disabled{opacity:0.6;cursor:wait}.ali-invoice-btn.done{background:#28a745}`; | |
| const style = document.createElement("style"); | |
| style.textContent = css; | |
| document.head.appendChild(style); | |
| const API = { | |
| QUERY: | |
| "https://trade.aliexpress.com/ajax/invoice/queryInvoiceIdAjax.htm?orderIds=", | |
| EXPORT: | |
| "https://trade.aliexpress.com/ajax/invoice/invoiceExportAjax.htm?invoiceId={id}&orderId={order}", | |
| }; | |
| setInterval(() => { | |
| if (location.pathname.includes("/detail.html")) scanDetails(); | |
| else scanList(); | |
| }, 1000); | |
| function scanDetails() { | |
| const orderId = new URLSearchParams(location.search).get("orderId"); | |
| if (!orderId) return; | |
| const buttons = document.querySelectorAll("button"); | |
| for (const btn of buttons) { | |
| if ( | |
| btn.textContent.includes("Receipt") && | |
| !btn.nextSibling?.dataset?.order | |
| ) { | |
| inject(btn.parentNode, orderId, btn.nextSibling); | |
| break; | |
| } | |
| } | |
| } | |
| function scanList() { | |
| const walker = document.createTreeWalker( | |
| document.body, | |
| NodeFilter.SHOW_TEXT, | |
| null, | |
| false, | |
| ); | |
| let node; | |
| while ((node = walker.nextNode())) { | |
| const match = node.textContent.match(/\b(\d{15,22})\b/); | |
| if (match) { | |
| const el = node.parentNode; | |
| if (["SCRIPT", "STYLE", "A"].includes(el.tagName)) continue; | |
| const txt = ( | |
| el.textContent + (el.parentElement?.textContent || "") | |
| ).toLowerCase(); | |
| if ( | |
| (txt.includes("order") || txt.includes("id")) && | |
| !el.querySelector(".ali-invoice-btn") | |
| ) { | |
| inject(el, match[1]); | |
| } | |
| } | |
| } | |
| } | |
| function inject(parent, orderId, beforeNode = null) { | |
| // Changed to <a> tag to support "Right Click -> Copy Link Address" | |
| const btn = document.createElement("a"); | |
| btn.className = "ali-invoice-btn"; | |
| btn.dataset.order = orderId; | |
| btn.textContent = "Invoice"; | |
| btn.title = "Hover to load URL"; | |
| btn.href = "javascript:void(0)"; // Placeholder | |
| btn.onclick = (e) => { | |
| // Prevent default navigation if we are still loading or if we want to handle visual feedback | |
| if (btn.href.startsWith("javascript")) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| download(btn, orderId); | |
| } else { | |
| // If ID is ready (href is real), we can let the browser handle it... | |
| // BUT we want the visual feedback "Done", so we intervene slightly. | |
| // We don't preventDefault here, so the download starts natively. | |
| visualSuccess(btn); | |
| } | |
| }; | |
| // Hover: Pre-fetch ID to populate href | |
| btn.onmouseenter = () => { | |
| if (btn.dataset.ready || btn.dataset.fetching) return; | |
| btn.dataset.fetching = "1"; | |
| resolveInvoiceId(orderId) | |
| .then((id) => { | |
| if (id) { | |
| const url = API.EXPORT.replace("{id}", id).replace( | |
| "{order}", | |
| orderId, | |
| ); | |
| btn.href = url; // SET REAL LINK -> Enables "Copy Link Address" | |
| btn.title = url; | |
| btn.dataset.ready = "1"; | |
| } else { | |
| btn.title = "No Invoice Found"; | |
| btn.textContent = "None"; // Visual cue | |
| } | |
| }) | |
| .catch((err) => { | |
| btn.title = "Error: " + err; | |
| }); | |
| }; | |
| if (beforeNode) parent.insertBefore(btn, beforeNode); | |
| else parent.appendChild(btn); | |
| } | |
| async function download(btn, orderId) { | |
| if (btn.classList.contains("disabled")) return; | |
| // Fetch if needed (user clicked without hovering) | |
| btn.classList.add("disabled"); | |
| try { | |
| const id = await resolveInvoiceId(orderId); | |
| if (!id) throw "No Invoice"; | |
| const url = API.EXPORT.replace("{id}", id).replace("{order}", orderId); | |
| // Navigate manually since href wasn't ready | |
| window.location.assign(url); | |
| visualSuccess(btn); | |
| } catch (err) { | |
| console.error(err); | |
| btn.textContent = "Err"; | |
| btn.title = err; | |
| btn.classList.remove("disabled"); | |
| if (err === "Log in expired") alert("Session Expired. Reload Page."); | |
| } | |
| } | |
| function visualSuccess(btn) { | |
| btn.textContent = "Downloading"; | |
| btn.classList.add("done"); | |
| setTimeout(() => { | |
| btn.classList.remove("disabled"); | |
| btn.textContent = "Invoice"; | |
| btn.classList.remove("done"); | |
| }, 3000); | |
| } | |
| function resolveInvoiceId(orderId) { | |
| return request(API.QUERY + orderId).then((res) => { | |
| if (res.responseText.includes("<html")) throw "Log in expired"; | |
| let json = null; | |
| try { | |
| json = JSON.parse(res.responseText); | |
| } catch (e) {} | |
| let invId = Array.isArray(json) | |
| ? json[0] | |
| : json?.invoiceId || json?.data?.invoiceId || json?.result?.invoiceId; | |
| if (!invId || invId === "null") return null; | |
| return invId; | |
| }); | |
| } | |
| function request(url) { | |
| return new Promise((resolve, reject) => { | |
| GM_xmlhttpRequest({ | |
| method: "GET", | |
| url, | |
| headers: { | |
| "X-Requested-With": "XMLHttpRequest", | |
| Referer: window.location.href, | |
| }, | |
| onload: resolve, | |
| onerror: reject, | |
| }); | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment