570 lines
19 KiB
JavaScript
570 lines
19 KiB
JavaScript
/**
|
||
* 界面元素引用 (UI References)
|
||
*/
|
||
const tabs = document.querySelectorAll(".tab-btn");
|
||
const tabContents = document.querySelectorAll(".tab-content");
|
||
const binInput = document.getElementById("binInput");
|
||
const addBinBtn = document.getElementById("addBinBtn");
|
||
const generateCardsBtn = document.getElementById("generateCardsBtn");
|
||
const statusMessage = document.getElementById("statusMessage");
|
||
const binHistoryList = document.getElementById("binHistoryList");
|
||
|
||
// 地址管理相关元素
|
||
const nameInput = document.getElementById("nameInput");
|
||
const address1Input = document.getElementById("address1Input");
|
||
const address2Input = document.getElementById("address2Input");
|
||
const cityInput = document.getElementById("cityInput");
|
||
const stateInput = document.getElementById("stateInput");
|
||
const zipInput = document.getElementById("zipInput");
|
||
const addAddressBtn = document.getElementById("addAddressBtn");
|
||
const addressesList = document.getElementById("addressesList");
|
||
|
||
// 姓名管理相关元素
|
||
const firstNameInput = document.getElementById("firstNameInput");
|
||
const lastNameInput = document.getElementById("lastNameInput");
|
||
const addNameBtn = document.getElementById("addNameBtn");
|
||
const namesList = document.getElementById("namesList");
|
||
|
||
// 设置相关元素
|
||
const useLuhnValidation = document.getElementById("useLuhnValidation");
|
||
const DEFAULT_BIN = "552461xxxxxxxxxx";
|
||
|
||
/**
|
||
* 格式化输入:BIN 输入框
|
||
* 自动转大写,移除非数字/X字符
|
||
*/
|
||
binInput.addEventListener("input", (e) => {
|
||
let val = e.target.value.toUpperCase();
|
||
val = val.replace(/[^0-9X]/g, "");
|
||
if (val.length > 19) {
|
||
val = val.substring(0, 19);
|
||
}
|
||
e.target.value = val;
|
||
});
|
||
|
||
/**
|
||
* 格式化输入:粘贴处理
|
||
* 自动补全 X,确保格式符合 16 位卡号长度
|
||
*/
|
||
binInput.addEventListener("blur", (e) => {
|
||
let val = e.target.value.trim().replace(/[^0-9X]/g, "");
|
||
// 如果长度不足且非空,自动补 X
|
||
if (val.length > 0 && val.length < 16) {
|
||
const currentLen = val.replace(/X/g, "").length;
|
||
const needed = 16 - currentLen;
|
||
val = val + "X".repeat(needed);
|
||
}
|
||
e.target.value = val;
|
||
});
|
||
|
||
// ==========================================
|
||
// 设置菜单逻辑
|
||
// ==========================================
|
||
const settingsToggleBtn = document.getElementById("settingsToggleBtn");
|
||
const miniSettings = document.getElementById("miniSettings");
|
||
|
||
if (settingsToggleBtn && miniSettings) {
|
||
settingsToggleBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
miniSettings.classList.toggle("show");
|
||
settingsToggleBtn.classList.toggle("active");
|
||
});
|
||
|
||
// 点击外部关闭菜单
|
||
document.addEventListener("click", (e) => {
|
||
if (!miniSettings.contains(e.target) && e.target !== settingsToggleBtn && !settingsToggleBtn.contains(e.target)) {
|
||
miniSettings.classList.remove("show");
|
||
settingsToggleBtn.classList.remove("active");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Luhn 校验开关
|
||
if (useLuhnValidation) {
|
||
useLuhnValidation.addEventListener("change", () => {
|
||
chrome.storage.local.set({
|
||
useLuhnValidation: useLuhnValidation.checked
|
||
});
|
||
});
|
||
// 初始化状态
|
||
chrome.storage.local.get(["useLuhnValidation"], (data) => {
|
||
if (data.useLuhnValidation !== undefined) {
|
||
useLuhnValidation.checked = data.useLuhnValidation;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 数据源选择器逻辑
|
||
const addressSourceSelect = document.getElementById("addressSourceSelect");
|
||
const nameSourceSelect = document.getElementById("nameSourceSelect");
|
||
|
||
if (addressSourceSelect) {
|
||
addressSourceSelect.addEventListener("change", () => {
|
||
const val = addressSourceSelect.value;
|
||
chrome.storage.local.set({ addressSource: val });
|
||
console.log("✅ Address source changed to:", val);
|
||
});
|
||
chrome.storage.local.get(["addressSource"], (data) => {
|
||
if (data.addressSource) {
|
||
addressSourceSelect.value = data.addressSource;
|
||
} else {
|
||
// 默认设置为 static
|
||
addressSourceSelect.value = "static";
|
||
chrome.storage.local.set({ addressSource: "static" });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (nameSourceSelect) {
|
||
nameSourceSelect.addEventListener("change", () => {
|
||
const val = nameSourceSelect.value;
|
||
chrome.storage.local.set({ nameSource: val });
|
||
console.log("✅ Name source changed to:", val);
|
||
});
|
||
chrome.storage.local.get(["nameSource"], (data) => {
|
||
if (data.nameSource) {
|
||
nameSourceSelect.value = data.nameSource;
|
||
} else {
|
||
nameSourceSelect.value = "static";
|
||
chrome.storage.local.set({ nameSource: "static" });
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化加载所有数据
|
||
loadData();
|
||
|
||
// ==========================================
|
||
// 标签页切换逻辑 (Tabs)
|
||
// ==========================================
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener("click", () => {
|
||
const targetId = tab.dataset.tab;
|
||
tabs.forEach(t => t.classList.remove("active"));
|
||
tabContents.forEach(c => c.classList.remove("active"));
|
||
|
||
tab.classList.add("active");
|
||
document.getElementById(targetId + "-tab").classList.add("active");
|
||
});
|
||
});
|
||
|
||
const subTabs = document.querySelectorAll(".sub-tab-btn");
|
||
const subTabContents = document.querySelectorAll(".sub-tab-content");
|
||
|
||
subTabs.forEach(tab => {
|
||
tab.addEventListener("click", () => {
|
||
const targetId = tab.dataset.subtab;
|
||
subTabs.forEach(t => t.classList.remove("active"));
|
||
subTabContents.forEach(c => c.classList.remove("active"));
|
||
|
||
tab.classList.add("active");
|
||
document.getElementById(targetId + "-subtab").classList.add("active");
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// 核心功能:添加 BIN 和 生成卡片
|
||
// ==========================================
|
||
|
||
addBinBtn.addEventListener("click", () => {
|
||
const bin = binInput.value.trim();
|
||
if (!bin) return;
|
||
|
||
chrome.storage.local.get(["binHistory"], (data) => {
|
||
let history = data.binHistory || [];
|
||
// 去重并添加到头部
|
||
history = history.filter(b => b !== bin);
|
||
history.unshift(bin);
|
||
// 限制历史记录数量
|
||
if (history.length > 20) history = history.slice(0, 20);
|
||
|
||
chrome.storage.local.set({
|
||
binHistory: history,
|
||
currentBin: bin
|
||
}, () => {
|
||
loadBinHistory();
|
||
showToast("BIN added to history");
|
||
});
|
||
});
|
||
});
|
||
|
||
generateCardsBtn.addEventListener("click", async () => {
|
||
const bin = binInput.value.trim();
|
||
if (!bin) {
|
||
showStatus("Please enter a BIN number", "error");
|
||
return;
|
||
}
|
||
if (bin.length < 6) {
|
||
showStatus("BIN must be at least 6 digits", "error");
|
||
return;
|
||
}
|
||
|
||
const useLuhn = useLuhnValidation.checked;
|
||
|
||
// UI 状态更新:处理中
|
||
generateCardsBtn.disabled = true;
|
||
generateCardsBtn.innerHTML = "<span class=\"btn-icon\">⏳</span><span>Processing...</span>";
|
||
|
||
if (useLuhn) {
|
||
showStatus("🔐 Generating cards with Luhn validation...", "loading");
|
||
} else {
|
||
showStatus("⚡ Generating cards...", "loading");
|
||
}
|
||
|
||
// 保存 BIN 历史
|
||
chrome.storage.local.get(["binHistory"], (data) => {
|
||
let history = data.binHistory || [];
|
||
history = history.filter(b => b !== bin);
|
||
history.unshift(bin);
|
||
if (history.length > 20) history = history.slice(0, 20);
|
||
chrome.storage.local.set({
|
||
binHistory: history,
|
||
currentBin: bin
|
||
}, () => {
|
||
loadBinHistory();
|
||
});
|
||
});
|
||
|
||
// 发送消息给后台生成卡片
|
||
chrome.runtime.sendMessage({
|
||
action: "generateCards",
|
||
bin: bin,
|
||
useValidation: useLuhn,
|
||
stripeTabId: null
|
||
}, (response) => {
|
||
if (response && response.success) {
|
||
const luhnMsg = useLuhn ? " (Luhn validated)" : "";
|
||
showStatus("✅ Generated " + response.cards.length + " cards" + luhnMsg + ". Filling form...", "loading");
|
||
|
||
// 查找 Stripe 标签页并注入填充指令
|
||
chrome.tabs.query({
|
||
url: ["https://checkout.stripe.com/*", "https://*.stripe.com/*"]
|
||
}, (tabs) => {
|
||
if (chrome.runtime.lastError) {
|
||
resetGenerateButton();
|
||
showStatus("❌ Error: " + chrome.runtime.lastError.message, "error");
|
||
return;
|
||
}
|
||
|
||
if (tabs.length > 0) {
|
||
// 优先选择当前激活的标签页
|
||
const targetTab = tabs.find(t => t.active) || tabs[0];
|
||
chrome.tabs.sendMessage(targetTab.id, {
|
||
action: "fillForm"
|
||
}, (fillResponse) => {
|
||
resetGenerateButton();
|
||
if (chrome.runtime.lastError) {
|
||
showStatus("❌ No Stripe checkout page found. Please open one first.", "error");
|
||
} else {
|
||
showStatus("Form filled!", "success");
|
||
showToast("✅ Form filled successfully!");
|
||
}
|
||
});
|
||
} else {
|
||
resetGenerateButton();
|
||
showStatus("❌ No Stripe checkout page found. Please open one first.", "error");
|
||
}
|
||
});
|
||
} else {
|
||
resetGenerateButton();
|
||
showStatus("Failed to generate cards", "error");
|
||
showToast("❌ Failed to generate cards. Try again.", "error");
|
||
}
|
||
});
|
||
});
|
||
|
||
function resetGenerateButton() {
|
||
generateCardsBtn.disabled = false;
|
||
generateCardsBtn.innerHTML = "<span class=\"btn-icon\">🚀</span><span>Fill Everything</span>";
|
||
}
|
||
|
||
// ==========================================
|
||
// 历史记录管理
|
||
// ==========================================
|
||
|
||
function loadBinHistory() {
|
||
chrome.storage.local.get(["binHistory", "currentBin"], (data) => {
|
||
const history = data.binHistory || [];
|
||
const current = data.currentBin || DEFAULT_BIN;
|
||
|
||
binInput.value = current;
|
||
binHistoryList.innerHTML = "";
|
||
|
||
if (history.length === 0) {
|
||
binHistoryList.innerHTML = "<div class=\"empty\">No BINs saved yet</div>";
|
||
return;
|
||
}
|
||
|
||
history.forEach(bin => {
|
||
const item = document.createElement("div");
|
||
item.className = "history-item";
|
||
|
||
const binText = document.createElement("span");
|
||
binText.textContent = bin;
|
||
binText.className = "history-bin";
|
||
binText.addEventListener("click", () => {
|
||
binInput.value = bin;
|
||
chrome.storage.local.set({ currentBin: bin });
|
||
showToast("BIN selected");
|
||
});
|
||
|
||
const delBtn = document.createElement("button");
|
||
delBtn.textContent = "×";
|
||
delBtn.className = "delete-btn";
|
||
delBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
deleteBin(bin);
|
||
});
|
||
|
||
item.appendChild(binText);
|
||
item.appendChild(delBtn);
|
||
binHistoryList.appendChild(item);
|
||
});
|
||
});
|
||
}
|
||
|
||
function deleteBin(bin) {
|
||
chrome.storage.local.get(["binHistory"], (data) => {
|
||
let history = data.binHistory || [];
|
||
history = history.filter(b => b !== bin);
|
||
chrome.storage.local.set({ binHistory: history }, () => {
|
||
loadBinHistory();
|
||
showToast("BIN deleted");
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// 地址管理
|
||
// ==========================================
|
||
|
||
addAddressBtn.addEventListener("click", () => {
|
||
const name = nameInput.value.trim();
|
||
const addr1 = address1Input.value.trim();
|
||
const addr2 = address2Input.value.trim();
|
||
const city = cityInput.value.trim();
|
||
const state = stateInput.value.trim();
|
||
const zip = zipInput.value.trim();
|
||
|
||
if (!name || !addr1 || !city || !state || !zip) {
|
||
showToast("Please fill all required fields", "error");
|
||
return;
|
||
}
|
||
|
||
// 简单的姓名拆分逻辑
|
||
const nameParts = name.split(" ");
|
||
const first = nameParts[0] || name;
|
||
const last = nameParts.slice(1).join(" ") || nameParts[0];
|
||
|
||
const newAddr = {
|
||
id: Date.now(),
|
||
name: name,
|
||
firstName: first,
|
||
lastName: last,
|
||
address1: addr1,
|
||
address2: addr2,
|
||
city: city,
|
||
state: state,
|
||
stateCode: getStateCode(state), // 转换州简写 (如 California -> CA)
|
||
postal: zip,
|
||
countryText: "United States",
|
||
countryValue: "US"
|
||
};
|
||
|
||
chrome.storage.local.get(["customAddresses"], (data) => {
|
||
const list = data.customAddresses || [];
|
||
list.push(newAddr);
|
||
chrome.storage.local.set({ customAddresses: list }, () => {
|
||
clearAddressInputs();
|
||
loadAddresses();
|
||
showToast("Address added");
|
||
});
|
||
});
|
||
});
|
||
|
||
function clearAddressInputs() {
|
||
nameInput.value = "";
|
||
address1Input.value = "";
|
||
address2Input.value = "";
|
||
cityInput.value = "";
|
||
stateInput.value = "";
|
||
zipInput.value = "";
|
||
}
|
||
|
||
function loadAddresses() {
|
||
chrome.storage.local.get(["customAddresses"], (data) => {
|
||
const list = data.customAddresses || [];
|
||
addressesList.innerHTML = "";
|
||
|
||
if (list.length === 0) {
|
||
addressesList.innerHTML = "<div class=\"empty\">No addresses saved yet</div>";
|
||
return;
|
||
}
|
||
|
||
list.forEach(addr => {
|
||
const item = document.createElement("div");
|
||
item.className = "list-item";
|
||
|
||
const info = document.createElement("div");
|
||
info.className = "item-info";
|
||
info.innerHTML = `
|
||
<strong>${addr.name}</strong><br>
|
||
<small>${addr.address1}${addr.address2 ? ", " + addr.address2 : ""}<br>
|
||
${addr.city}, ${addr.state} ${addr.postal}</small>
|
||
`;
|
||
|
||
const delBtn = document.createElement("button");
|
||
delBtn.textContent = "×";
|
||
delBtn.className = "delete-btn";
|
||
delBtn.addEventListener("click", () => deleteAddress(addr.id));
|
||
|
||
item.appendChild(info);
|
||
item.appendChild(delBtn);
|
||
addressesList.appendChild(item);
|
||
});
|
||
});
|
||
}
|
||
|
||
function deleteAddress(id) {
|
||
chrome.storage.local.get(["customAddresses"], (data) => {
|
||
let list = data.customAddresses || [];
|
||
list = list.filter(item => item.id !== id);
|
||
chrome.storage.local.set({ customAddresses: list }, () => {
|
||
loadAddresses();
|
||
showToast("Address deleted");
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// 姓名管理
|
||
// ==========================================
|
||
|
||
addNameBtn.addEventListener("click", () => {
|
||
const first = firstNameInput.value.trim();
|
||
const last = lastNameInput.value.trim();
|
||
|
||
if (!first || !last) {
|
||
showToast("Please enter both first and last name", "error");
|
||
return;
|
||
}
|
||
|
||
const newName = {
|
||
id: Date.now(),
|
||
firstName: first,
|
||
lastName: last,
|
||
fullName: first + " " + last
|
||
};
|
||
|
||
chrome.storage.local.get(["customNames"], (data) => {
|
||
const list = data.customNames || [];
|
||
list.push(newName);
|
||
chrome.storage.local.set({ customNames: list }, () => {
|
||
firstNameInput.value = "";
|
||
lastNameInput.value = "";
|
||
loadNames();
|
||
showToast("Name added");
|
||
});
|
||
});
|
||
});
|
||
|
||
function loadNames() {
|
||
chrome.storage.local.get(["customNames"], (data) => {
|
||
const list = data.customNames || [];
|
||
namesList.innerHTML = "";
|
||
|
||
if (list.length === 0) {
|
||
namesList.innerHTML = "<div class=\"empty\">No names saved yet</div>";
|
||
return;
|
||
}
|
||
|
||
list.forEach(item => {
|
||
const div = document.createElement("div");
|
||
div.className = "list-item";
|
||
|
||
const info = document.createElement("div");
|
||
info.className = "item-info";
|
||
info.innerHTML = `<strong>${item.fullName}</strong>`;
|
||
|
||
const delBtn = document.createElement("button");
|
||
delBtn.textContent = "×";
|
||
delBtn.className = "delete-btn";
|
||
delBtn.addEventListener("click", () => deleteName(item.id));
|
||
|
||
div.appendChild(info);
|
||
div.appendChild(delBtn);
|
||
namesList.appendChild(div);
|
||
});
|
||
});
|
||
}
|
||
|
||
function deleteName(id) {
|
||
chrome.storage.local.get(["customNames"], (data) => {
|
||
let list = data.customNames || [];
|
||
list = list.filter(item => item.id !== id);
|
||
chrome.storage.local.set({ customNames: list }, () => {
|
||
loadNames();
|
||
showToast("Name deleted");
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// 辅助工具函数
|
||
// ==========================================
|
||
|
||
/**
|
||
* 将州全称转换为两字母缩写 (用于表单匹配)
|
||
*/
|
||
function getStateCode(stateName) {
|
||
const map = {
|
||
Alabama: "AL", Alaska: "AK", Arizona: "AZ", Arkansas: "AR", California: "CA",
|
||
Colorado: "CO", Connecticut: "CT", Delaware: "DE", Florida: "FL", Georgia: "GA",
|
||
Hawaii: "HI", Idaho: "ID", Illinois: "IL", Indiana: "IN", Iowa: "IA",
|
||
Kansas: "KS", Kentucky: "KY", Louisiana: "LA", Maine: "ME", Maryland: "MD",
|
||
Massachusetts: "MA", Michigan: "MI", Minnesota: "MN", Mississippi: "MS", Missouri: "MO",
|
||
Montana: "MT", Nebraska: "NE", Nevada: "NV", "New Hampshire": "NH", "New Jersey": "NJ",
|
||
"New Mexico": "NM", "New York": "NY", "North Carolina": "NC", "North Dakota": "ND", Ohio: "OH",
|
||
Oklahoma: "OK", Oregon: "OR", Pennsylvania: "PA", "Rhode Island": "RI", "South Carolina": "SC",
|
||
"South Dakota": "SD", Tennessee: "TN", Texas: "TX", Utah: "UT", Vermont: "VT",
|
||
Virginia: "VA", Washington: "WA", "West Virginia": "WV", Wisconsin: "WI", Wyoming: "WY"
|
||
};
|
||
return map[stateName] || stateName.substring(0, 2).toUpperCase();
|
||
}
|
||
|
||
function showStatus(msg, type = "") {
|
||
statusMessage.textContent = msg;
|
||
statusMessage.className = "status-message " + type;
|
||
statusMessage.style.display = "block";
|
||
|
||
if (type === "success" || type === "error") {
|
||
setTimeout(() => {
|
||
statusMessage.style.display = "none";
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
function showToast(msg, type = "success") {
|
||
const toast = document.createElement("div");
|
||
toast.className = "toast " + type;
|
||
toast.textContent = msg;
|
||
document.body.appendChild(toast);
|
||
|
||
// 动画显示
|
||
setTimeout(() => {
|
||
toast.classList.add("show");
|
||
}, 10);
|
||
|
||
// 自动消失
|
||
setTimeout(() => {
|
||
toast.classList.remove("show");
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 2000);
|
||
}
|
||
|
||
function loadData() {
|
||
loadBinHistory();
|
||
loadAddresses();
|
||
loadNames();
|
||
}
|