Skip to content

Instantly share code, notes, and snippets.

@dzid26
Last active December 5, 2025 16:58
Show Gist options
  • Select an option

  • Save dzid26/d81fbaf22e6964d936b5ea44359d97f1 to your computer and use it in GitHub Desktop.

Select an option

Save dzid26/d81fbaf22e6964d936b5ea44359d97f1 to your computer and use it in GitHub Desktop.
revert to simple download
// ==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