881 lines
31 KiB
JavaScript
881 lines
31 KiB
JavaScript
/**
|
||
* 全局状态标记
|
||
*/
|
||
let isProcessing = false;
|
||
let fillButton = null;
|
||
let clearButton = null;
|
||
|
||
/**
|
||
* 默认地址数据源
|
||
*/
|
||
const DEFAULT_ADDRESSES = [{
|
||
name: "John Smith",
|
||
firstName: "John",
|
||
lastName: "Smith",
|
||
address1: "69 Adams Street",
|
||
address2: "",
|
||
city: "Brooklyn",
|
||
state: "New York",
|
||
stateCode: "NY",
|
||
postal: "11201",
|
||
countryText: "United States",
|
||
countryValue: "US"
|
||
}, {
|
||
name: "Michael Johnson",
|
||
firstName: "Michael",
|
||
lastName: "Johnson",
|
||
address1: "3511 Carlisle Avenue",
|
||
address2: "",
|
||
city: "Covington",
|
||
state: "Kentucky",
|
||
stateCode: "KY",
|
||
postal: "41015",
|
||
countryText: "United States",
|
||
countryValue: "US"
|
||
}];
|
||
|
||
/**
|
||
* 字段匹配同义词词典
|
||
* 用于启发式识别表单字段类型
|
||
*/
|
||
const FIELD_SYNONYMS = {
|
||
fullName: ["full name", "name", "cardholder name", "card name", "cc-name"],
|
||
firstName: ["first name", "given-name"],
|
||
lastName: ["last name", "family-name", "surname"],
|
||
address1: ["address", "address line 1", "street", "addressline1", "address-line1", "address-line-1"],
|
||
address2: ["address line 2", "apt", "apartment", "addressline2", "address-line2", "suite"],
|
||
city: ["city", "locality", "address-level2"],
|
||
state: ["state", "region", "province", "administrative area", "address-level1", "address level 1"],
|
||
postal: ["postal", "zip", "postcode", "postal-code"],
|
||
country: ["country", "country or region"]
|
||
};
|
||
|
||
const CARD_FIELD_WORDS = ["card", "cvc", "cvv", "expiry", "expiration", "valid thru", "month", "year"];
|
||
|
||
// ==========================================
|
||
// 核心工具函数
|
||
// ==========================================
|
||
|
||
/**
|
||
* 获取随机地址
|
||
* 优先从 storage 读取配置,支持 static/manual/auto 模式
|
||
*/
|
||
async function getRandomAddress() {
|
||
return new Promise(resolve => {
|
||
chrome.storage.local.get(["customAddresses", "addressSource"], data => {
|
||
const customList = data.customAddresses || [];
|
||
const sourceMode = data.addressSource || "static";
|
||
let addressPool = [];
|
||
|
||
switch (sourceMode) {
|
||
case "static":
|
||
addressPool = DEFAULT_ADDRESSES;
|
||
break;
|
||
case "manual":
|
||
addressPool = customList.length > 0 ? customList : DEFAULT_ADDRESSES;
|
||
break;
|
||
case "auto":
|
||
addressPool = DEFAULT_ADDRESSES;
|
||
break;
|
||
default:
|
||
addressPool = DEFAULT_ADDRESSES;
|
||
}
|
||
|
||
if (addressPool.length === 0) {
|
||
resolve(DEFAULT_ADDRESSES[0]);
|
||
} else {
|
||
const selected = addressPool[Math.floor(Math.random() * addressPool.length)];
|
||
console.log(`[cardbingenerator] Using ${sourceMode} address:`, selected.name);
|
||
resolve(selected);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
function randomDelay(min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
/**
|
||
* 模拟人类输入文本
|
||
* @param {HTMLElement} element - 目标输入框
|
||
* @param {string} text - 要输入的文本
|
||
* @param {boolean} clearFirst - 是否先清空输入框 (默认 false)
|
||
*/
|
||
async function typeText(element, text, clearFirst = false) {
|
||
if (!element || !text) return;
|
||
|
||
element.focus();
|
||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
await sleep(randomDelay(150, 300));
|
||
|
||
if (clearFirst && text.length < 50) {
|
||
// 模拟逐字删除或清空
|
||
element.value = "";
|
||
for (let i = 0; i < text.length; i++) {
|
||
element.value += text[i];
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
await sleep(randomDelay(30, 80)); // 模拟击键间隔
|
||
}
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
} else {
|
||
// 直接赋值模式
|
||
element.value = text;
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}
|
||
|
||
await sleep(randomDelay(100, 200));
|
||
element.blur();
|
||
await sleep(randomDelay(200, 400));
|
||
}
|
||
|
||
/**
|
||
* 收集页面所有根节点(包括 Shadow DOM)
|
||
* 用于穿透 Shadow DOM 查找元素
|
||
*/
|
||
function collectRoots() {
|
||
const roots = [document];
|
||
const queue = [document.documentElement];
|
||
|
||
while (queue.length) {
|
||
const node = queue.pop();
|
||
if (!node) continue;
|
||
|
||
if (node.shadowRoot) {
|
||
roots.push(node.shadowRoot);
|
||
}
|
||
|
||
const children = node.children || [];
|
||
for (let i = 0; i < children.length; i++) {
|
||
queue.push(children[i]);
|
||
}
|
||
}
|
||
return roots;
|
||
}
|
||
|
||
/**
|
||
* 检测元素是否可见且可交互
|
||
*/
|
||
function isVisible(el) {
|
||
if (!el) return false;
|
||
const rect = el.getBoundingClientRect();
|
||
const style = window.getComputedStyle(el);
|
||
|
||
if (style.visibility === "hidden" || style.display === "none") return false;
|
||
if (el.disabled) return false;
|
||
if (rect.width <= 0 || rect.height <= 0) return false;
|
||
if (el.type === "hidden") return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 收集页面所有可见的表单元素 (input, select, textarea)
|
||
*/
|
||
function collectFormElements() {
|
||
const elements = [];
|
||
for (const root of collectRoots()) {
|
||
const nodes = root.querySelectorAll("input, select, textarea");
|
||
nodes.forEach(node => {
|
||
if (isVisible(node)) {
|
||
elements.push(node);
|
||
}
|
||
});
|
||
}
|
||
return elements;
|
||
}
|
||
|
||
/**
|
||
* 使用原生 Setter 设置值并触发事件
|
||
* 绕过 React/Vue 等框架的状态绑定限制
|
||
*/
|
||
async function setNativeValueAndDispatch(element, value, simulateTyping = false) {
|
||
if (!element) return;
|
||
|
||
try {
|
||
element.focus();
|
||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
await sleep(randomDelay(150, 300));
|
||
|
||
const tagName = element.tagName;
|
||
|
||
if (tagName === "INPUT" || tagName === "TEXTAREA") {
|
||
const proto = tagName === "INPUT" ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
|
||
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value").set;
|
||
|
||
if (simulateTyping && value && value.length < 30) {
|
||
// 模拟打字效果
|
||
element.value = "";
|
||
for (let i = 0; i < value.length; i++) {
|
||
nativeSetter.call(element, element.value + value[i]);
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
await sleep(randomDelay(50, 120));
|
||
}
|
||
} else {
|
||
// 快速填充
|
||
nativeSetter.call(element, value);
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
}
|
||
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
await sleep(randomDelay(150, 250));
|
||
element.blur();
|
||
|
||
} else if (tagName === "SELECT") {
|
||
element.value = value;
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
await sleep(randomDelay(200, 350));
|
||
element.blur();
|
||
}
|
||
|
||
await sleep(randomDelay(300, 500));
|
||
|
||
} catch (err) {
|
||
// 降级处理:直接赋值
|
||
try {
|
||
element.focus();
|
||
await sleep(randomDelay(150, 300));
|
||
element.value = value;
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
await sleep(randomDelay(150, 250));
|
||
element.blur();
|
||
await sleep(randomDelay(300, 500));
|
||
} catch (ignored) {}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 Select 下拉框的值选择
|
||
* 尝试匹配 value, textContent 或模糊匹配
|
||
*/
|
||
function pickSelectValue(selectEl, valueOptions, textOptions) {
|
||
if (!selectEl || selectEl.tagName !== "SELECT") return null;
|
||
|
||
const options = Array.from(selectEl.options || []);
|
||
const normalize = str => (str || "").toLowerCase().replace(/\s+/g, " ").trim();
|
||
const includesText = (text, query) => normalize(text).includes(normalize(query));
|
||
|
||
// 1. 尝试匹配 value
|
||
for (const val of valueOptions || []) {
|
||
const found = options.find(opt => normalize(opt.value) === normalize(val));
|
||
if (found) return found.value;
|
||
}
|
||
|
||
// 2. 尝试匹配 textContent (精确)
|
||
for (const txt of textOptions || []) {
|
||
const found = options.find(opt => normalize(opt.textContent) === normalize(txt));
|
||
if (found) return found.value;
|
||
}
|
||
|
||
// 3. 尝试匹配 textContent (包含)
|
||
for (const txt of textOptions || []) {
|
||
const found = options.find(opt => includesText(opt.textContent, txt));
|
||
if (found) return found.value;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// ==========================================
|
||
// 字段识别逻辑
|
||
// ==========================================
|
||
|
||
function getElementTextAttributes(el) {
|
||
const attrs = [
|
||
el.getAttribute("name"),
|
||
el.getAttribute("id"),
|
||
el.getAttribute("placeholder"),
|
||
el.getAttribute("aria-label"),
|
||
el.getAttribute("autocomplete"),
|
||
el.getAttribute("data-testid"),
|
||
el.getAttribute("data-qa")
|
||
].filter(Boolean);
|
||
return attrs.join(" ").toLowerCase();
|
||
}
|
||
|
||
function matchesAny(text, keywords) {
|
||
const lowerText = text.toLowerCase();
|
||
return keywords.some(kw => lowerText.includes(kw.toLowerCase()));
|
||
}
|
||
|
||
function isCardField(el) {
|
||
const text = getElementTextAttributes(el);
|
||
if (!text) return false;
|
||
return matchesAny(text, CARD_FIELD_WORDS);
|
||
}
|
||
|
||
/**
|
||
* 计算字段与特定类型的匹配分数
|
||
*/
|
||
function scoreForSynonyms(el, synonyms) {
|
||
const text = getElementTextAttributes(el);
|
||
let score = 0;
|
||
if (!text) return score;
|
||
|
||
const autocomplete = (el.getAttribute("autocomplete") || "").toLowerCase();
|
||
|
||
// 权重最高:autocomplete 属性匹配
|
||
for (const syn of synonyms) {
|
||
if (autocomplete.split(/\s+/).includes(syn.toLowerCase())) {
|
||
score += 6;
|
||
}
|
||
}
|
||
|
||
// 权重中等:name 或 id 包含关键词
|
||
const nameId = [(el.getAttribute("name") || "").toLowerCase(), (el.getAttribute("id") || "").toLowerCase()].join(" ");
|
||
for (const syn of synonyms) {
|
||
if (nameId.includes(syn.toLowerCase())) {
|
||
score += 4;
|
||
}
|
||
}
|
||
|
||
// 权重最低:placeholder 或 aria-label 包含关键词
|
||
const desc = [(el.getAttribute("placeholder") || "").toLowerCase(), (el.getAttribute("aria-label") || "").toLowerCase()].join(" ");
|
||
for (const syn of synonyms) {
|
||
if (desc.includes(syn.toLowerCase())) {
|
||
score += 2;
|
||
}
|
||
}
|
||
|
||
return score;
|
||
}
|
||
|
||
/**
|
||
* 寻找最佳匹配字段
|
||
*/
|
||
function findBestField(elements, synonyms, validator) {
|
||
let bestEl = null;
|
||
let bestScore = 0;
|
||
|
||
for (const el of elements) {
|
||
if (validator && !validator(el)) continue;
|
||
|
||
const score = scoreForSynonyms(el, synonyms);
|
||
if (score > bestScore) {
|
||
bestEl = el;
|
||
bestScore = score;
|
||
}
|
||
}
|
||
return bestEl;
|
||
}
|
||
|
||
/**
|
||
* 识别所有非卡号类的普通表单字段 (姓名、地址等)
|
||
*/
|
||
function detectAddressFields() {
|
||
const formElements = collectFormElements().filter(el => !isCardField(el));
|
||
const fields = {};
|
||
|
||
fields.firstName = findBestField(formElements, FIELD_SYNONYMS.firstName, el => el.tagName !== "SELECT");
|
||
fields.lastName = findBestField(formElements, FIELD_SYNONYMS.lastName, el => el.tagName !== "SELECT");
|
||
fields.fullName = findBestField(formElements, FIELD_SYNONYMS.fullName, el => el.tagName !== "SELECT");
|
||
fields.address1 = findBestField(formElements, FIELD_SYNONYMS.address1, el => el.tagName !== "SELECT");
|
||
fields.address2 = findBestField(formElements, FIELD_SYNONYMS.address2, el => el.tagName !== "SELECT");
|
||
fields.city = findBestField(formElements, FIELD_SYNONYMS.city, el => el.tagName !== "SELECT");
|
||
fields.state = findBestField(formElements, FIELD_SYNONYMS.state, el => el.tagName !== "SELECT");
|
||
fields.postal = findBestField(formElements, FIELD_SYNONYMS.postal, el => el.tagName !== "SELECT");
|
||
fields.country = findBestField(formElements, FIELD_SYNONYMS.country, () => true); // Country 可以是 select
|
||
|
||
// 逻辑修正:避免全名和名/姓重复匹配
|
||
if (fields.fullName) {
|
||
if (fields.firstName === fields.fullName) fields.firstName = null;
|
||
if (fields.lastName === fields.fullName) fields.lastName = null;
|
||
}
|
||
if (fields.firstName && fields.lastName && fields.firstName === fields.lastName) {
|
||
fields.fullName = fields.firstName;
|
||
fields.firstName = null;
|
||
fields.lastName = null;
|
||
}
|
||
|
||
return fields;
|
||
}
|
||
|
||
/**
|
||
* 专门识别信用卡相关字段
|
||
* 使用 CSS 选择器优先匹配
|
||
*/
|
||
function detectCardFields() {
|
||
const roots = collectRoots();
|
||
let numEl, expEl, cvcEl;
|
||
|
||
const numSelectors = ["input[autocomplete=\"cc-number\"]", "input[name*=\"cardnumber\" i]", "input[id*=\"cardnumber\" i]", "input[name=\"cardNumber\"]", "input[placeholder*=\"1234\"]", "#cardNumber"];
|
||
const expSelectors = ["input[autocomplete=\"cc-exp\"]", "input[name*=\"exp\" i]", "input[id*=\"exp\" i]", "input[placeholder*=\"MM\"]", "input[name=\"cardExpiry\"]", "#cardExpiry"];
|
||
const cvcSelectors = ["input[autocomplete=\"cc-csc\"]", "input[name*=\"cvc\" i]", "input[name*=\"cvv\" i]", "input[id*=\"cvc\" i]", "input[placeholder*=\"CVC\"]", "input[name=\"cardCvc\"]", "#cardCvc"];
|
||
|
||
function findInRoots(selectors) {
|
||
for (const root of roots) {
|
||
for (const sel of selectors) {
|
||
const el = root.querySelector(sel);
|
||
if (el && isVisible(el)) return el;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
numEl = findInRoots(numSelectors);
|
||
expEl = findInRoots(expSelectors);
|
||
cvcEl = findInRoots(cvcSelectors);
|
||
|
||
if (numEl || expEl || cvcEl) {
|
||
return { number: numEl, exp: expEl, cvc: cvcEl };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 自动点击“手动输入地址”按钮(如果有的话)
|
||
* 常见于 Google Places Autocomplete 覆盖了原生输入框的情况
|
||
*/
|
||
function clickManualAddressIfPresent() {
|
||
const keywords = ["enter address manually", "manually enter address", "ввести адрес вручную", "введите адрес вручную", "адрес вручную"];
|
||
try {
|
||
const roots = collectRoots();
|
||
const tagSelectors = ["button", "[role=\"button\"]", "a", ".Button", ".Link", "span[role=\"button\"]", "div[role=\"button\"]"];
|
||
|
||
for (const root of roots) {
|
||
for (const selector of tagSelectors) {
|
||
const elements = root.querySelectorAll(selector);
|
||
for (let i = 0; i < elements.length; i++) {
|
||
const el = elements[i];
|
||
if (!isVisible(el)) continue;
|
||
|
||
const text = [
|
||
el.textContent || "",
|
||
el.getAttribute("aria-label") || "",
|
||
el.getAttribute("title") || "",
|
||
el.getAttribute("data-testid") || ""
|
||
].join(" ").toLowerCase();
|
||
|
||
if (!text) continue;
|
||
|
||
if (keywords.some(kw => text.includes(kw.toLowerCase()))) {
|
||
const clickable = el.closest("button, [role=\"button\"], a, [role=\"link\"]") || el;
|
||
console.log("[cardbingenerator] Clicking manual address button:", el.textContent);
|
||
clickable.click();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {}
|
||
return false;
|
||
}
|
||
|
||
// ==========================================
|
||
// 主业务逻辑:自动填充流程
|
||
// ==========================================
|
||
|
||
async function autofillAll() {
|
||
if (isProcessing) return;
|
||
isProcessing = true;
|
||
|
||
try {
|
||
showNotification("🔄 Starting auto-fill...", "info");
|
||
await sleep(randomDelay(500, 1000));
|
||
|
||
// 1. 生成新卡片
|
||
showNotification("🔄 Generating fresh cards...", "info");
|
||
const storage = await chrome.storage.local.get(["currentBin"]);
|
||
const currentBin = storage.currentBin || "552461xxxxxxxxxx";
|
||
|
||
// 发送消息给 background script 生成卡片
|
||
const genResult = await new Promise(resolve => {
|
||
chrome.runtime.sendMessage({
|
||
action: "generateCards",
|
||
bin: currentBin,
|
||
stripeTabId: null
|
||
}, response => resolve(response));
|
||
});
|
||
|
||
if (!genResult || !genResult.success) {
|
||
showNotification("❌ Failed to generate cards: " + (genResult?.error || "Unknown error"), "error");
|
||
isProcessing = false;
|
||
return;
|
||
}
|
||
|
||
await sleep(2000);
|
||
|
||
// 2. 获取生成的卡片数据
|
||
const cardStorage = await chrome.storage.local.get(["generatedCards"]);
|
||
if (!cardStorage.generatedCards || cardStorage.generatedCards.length === 0) {
|
||
showNotification("❌ No cards were generated", "error");
|
||
isProcessing = false;
|
||
return;
|
||
}
|
||
|
||
const selectedCard = cardStorage.generatedCards[Math.floor(Math.random() * cardStorage.generatedCards.length)];
|
||
const addressData = await getRandomAddress();
|
||
|
||
showNotification("💳 Filling card details...", "info");
|
||
await sleep(randomDelay(400, 700));
|
||
|
||
// 3. 处理 Stripe 常见的折叠卡片按钮
|
||
const accordionBtn = document.querySelector("[data-testid=\"card-accordion-item-button\"]");
|
||
if (accordionBtn && isVisible(accordionBtn)) {
|
||
console.log("[cardbingenerator] Clicking card button...");
|
||
accordionBtn.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
await sleep(randomDelay(300, 500));
|
||
accordionBtn.click();
|
||
await sleep(randomDelay(800, 1200));
|
||
}
|
||
|
||
// 4. 填充卡片信息
|
||
const cardFields = detectCardFields();
|
||
if (cardFields) {
|
||
if (cardFields.number) {
|
||
console.log("[cardbingenerator] Filling card number...");
|
||
await setNativeValueAndDispatch(cardFields.number, selectedCard.card_number);
|
||
}
|
||
if (cardFields.exp) {
|
||
console.log("[cardbingenerator] Filling expiry date...");
|
||
const expStr = selectedCard.expiry_month + " / " + selectedCard.expiry_year.slice(-2);
|
||
await setNativeValueAndDispatch(cardFields.exp, expStr);
|
||
}
|
||
if (cardFields.cvc) {
|
||
console.log("[cardbingenerator] Filling CVC...");
|
||
await setNativeValueAndDispatch(cardFields.cvc, selectedCard.cvv);
|
||
}
|
||
}
|
||
|
||
// 5. 填充地址信息
|
||
showNotification("📝 Filling address...", "info");
|
||
const addrFields = detectAddressFields();
|
||
|
||
// 优先处理国家选择,因为这可能会刷新表单格式
|
||
if (addrFields.country && addrFields.country.tagName === "SELECT") {
|
||
console.log("[cardbingenerator] Filling country...");
|
||
const countryVal = pickSelectValue(addrFields.country, [addressData.countryValue], [addressData.countryText]);
|
||
if (countryVal) {
|
||
await setNativeValueAndDispatch(addrFields.country, countryVal);
|
||
}
|
||
await sleep(300);
|
||
}
|
||
|
||
// 检查是否有“手动输入地址”按钮并点击
|
||
const clickedManual = clickManualAddressIfPresent();
|
||
if (clickedManual) {
|
||
console.log("[cardbingenerator] Manual address button clicked");
|
||
await sleep(randomDelay(800, 1200));
|
||
}
|
||
|
||
// 填充详细地址字段
|
||
const fillAddressDetails = async () => {
|
||
const currentFields = detectAddressFields(); // 重新检测,因为 DOM 可能已变化
|
||
|
||
// 名字处理逻辑
|
||
if (currentFields.firstName && currentFields.lastName && currentFields.firstName !== currentFields.lastName) {
|
||
console.log("[cardbingenerator] Filling first name...");
|
||
await setNativeValueAndDispatch(currentFields.firstName, addressData.firstName, true);
|
||
console.log("[cardbingenerator] Filling last name...");
|
||
await setNativeValueAndDispatch(currentFields.lastName, addressData.lastName, true);
|
||
} else {
|
||
const nameField = currentFields.fullName || currentFields.firstName || currentFields.lastName;
|
||
if (nameField) {
|
||
console.log("[cardbingenerator] Filling full name...");
|
||
await setNativeValueAndDispatch(nameField, addressData.name, true);
|
||
}
|
||
}
|
||
|
||
if (currentFields.address1) {
|
||
console.log("[cardbingenerator] Filling address line 1...");
|
||
await setNativeValueAndDispatch(currentFields.address1, addressData.address1, false);
|
||
}
|
||
if (currentFields.address2) {
|
||
console.log("[cardbingenerator] Filling address line 2...");
|
||
await setNativeValueAndDispatch(currentFields.address2, addressData.address2, false);
|
||
}
|
||
if (currentFields.city) {
|
||
console.log("[cardbingenerator] Filling city...");
|
||
await setNativeValueAndDispatch(currentFields.city, addressData.city, true);
|
||
}
|
||
if (currentFields.postal) {
|
||
console.log("[cardbingenerator] Filling postal code...");
|
||
await setNativeValueAndDispatch(currentFields.postal, addressData.postal, false);
|
||
}
|
||
};
|
||
|
||
await fillAddressDetails();
|
||
|
||
// 延迟填充 State/Province,因为有些表单需要先填 Country/Zip 才会出现 State
|
||
const fillState = async () => {
|
||
await sleep(randomDelay(400, 600));
|
||
const fieldsNow = detectAddressFields();
|
||
if (!fieldsNow.state) return;
|
||
|
||
if (fieldsNow.state.tagName === "SELECT") {
|
||
console.log("📍 Filling state (select)...");
|
||
const stateVal = pickSelectValue(fieldsNow.state, [addressData.stateCode], [addressData.state]);
|
||
if (stateVal) {
|
||
await setNativeValueAndDispatch(fieldsNow.state, stateVal);
|
||
}
|
||
} else {
|
||
console.log("📍 Filling state (input)...");
|
||
await setNativeValueAndDispatch(fieldsNow.state, addressData.stateCode || addressData.state, true);
|
||
}
|
||
};
|
||
|
||
await fillState();
|
||
await sleep(randomDelay(600, 1000));
|
||
|
||
// 清理已使用的卡片数据
|
||
chrome.storage.local.remove(["generatedCards"], () => {
|
||
console.log("[cardbingenerator] Cleared used cards from storage");
|
||
});
|
||
|
||
console.log("[cardbingenerator] Auto-fill completed!");
|
||
showNotification("✅ All fields filled successfully!", "success");
|
||
|
||
} catch (err) {
|
||
chrome.storage.local.remove(["generatedCards"]);
|
||
showNotification("❌ Error: " + err.message, "error");
|
||
console.error("Autofill error:", err);
|
||
}
|
||
isProcessing = false;
|
||
}
|
||
|
||
// ==========================================
|
||
// UI 辅助功能:通知和清理按钮
|
||
// ==========================================
|
||
|
||
function showNotification(message, type = "info") {
|
||
const existing = document.getElementById("auto-card-filler-notification");
|
||
if (existing) existing.remove();
|
||
|
||
const notif = document.createElement("div");
|
||
notif.id = "auto-card-filler-notification";
|
||
notif.textContent = message;
|
||
|
||
const colors = {
|
||
info: "#3498db",
|
||
success: "#2ecc71",
|
||
warning: "#f39c12",
|
||
error: "#e74c3c"
|
||
};
|
||
|
||
Object.assign(notif.style, {
|
||
position: "fixed",
|
||
top: "20px",
|
||
right: "20px",
|
||
background: colors[type] || colors.info,
|
||
color: "white",
|
||
padding: "15px 20px",
|
||
borderRadius: "8px",
|
||
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
||
zIndex: "9999999",
|
||
fontSize: "14px",
|
||
fontWeight: "600",
|
||
maxWidth: "300px",
|
||
animation: "slideIn 0.3s ease-out"
|
||
});
|
||
|
||
const styleEl = document.createElement("style");
|
||
styleEl.textContent = `
|
||
@keyframes slideIn {
|
||
from { transform: translateX(400px); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
`;
|
||
|
||
if (!document.getElementById("autofill-notification-style")) {
|
||
styleEl.id = "autofill-notification-style";
|
||
document.head.appendChild(styleEl);
|
||
}
|
||
|
||
document.body.appendChild(notif);
|
||
|
||
setTimeout(() => {
|
||
notif.style.transition = "all 0.3s ease-out";
|
||
notif.style.transform = "translateX(400px)";
|
||
notif.style.opacity = "0";
|
||
setTimeout(() => notif.remove(), 300);
|
||
}, 5000);
|
||
}
|
||
|
||
function findPaymentMethodHeader() {
|
||
const roots = collectRoots();
|
||
for (const root of roots) {
|
||
const headers = root.querySelectorAll("h1, h2, h3, h4, .Header, [class*=\"header\"], [class*=\"title\"], [class*=\"Title\"]");
|
||
for (const h of headers) {
|
||
const text = h.textContent.toLowerCase().trim();
|
||
if (["payment method", "payment", "метод оплаты", "способ оплаты"].includes(text) || text.includes("payment method")) {
|
||
return h;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function createFillButton() {
|
||
if (clearButton || document.getElementById("stripe-clear-btn")) return;
|
||
|
||
const header = findPaymentMethodHeader();
|
||
clearButton = document.createElement("button");
|
||
clearButton.id = "stripe-clear-btn";
|
||
clearButton.innerHTML = "🗑️ Clear All Data";
|
||
|
||
// 注入按钮样式
|
||
clearButton.style.cssText = `
|
||
background: #dc3545;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
|
||
transition: all 0.2s ease;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
margin-left: 8px;
|
||
vertical-align: middle;
|
||
white-space: nowrap;
|
||
`;
|
||
|
||
clearButton.addEventListener("mouseenter", () => {
|
||
clearButton.style.transform = "translateY(-1px)";
|
||
clearButton.style.boxShadow = "0 4px 12px rgba(220, 53, 69, 0.4)";
|
||
});
|
||
|
||
clearButton.addEventListener("mouseleave", () => {
|
||
clearButton.style.transform = "translateY(0)";
|
||
clearButton.style.boxShadow = "0 2px 8px rgba(220, 53, 69, 0.3)";
|
||
});
|
||
|
||
clearButton.addEventListener("click", async () => {
|
||
if (confirm(`⚠️ Clear all Stripe data? This will:\n\n• Delete all cookies\n• Clear localStorage\n• Clear sessionStorage\n• Clear cache\n• Reload the page\n\nContinue?`)) {
|
||
clearButton.disabled = true;
|
||
clearButton.innerHTML = "⏳ Clearing...";
|
||
await clearAllStripeData();
|
||
showNotification("✅ All data cleared! Reloading...", "success");
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
}
|
||
});
|
||
|
||
// 尝试将按钮插入到支付标题旁边,如果找不到标题则悬浮显示
|
||
if (header) {
|
||
if (header.parentElement) {
|
||
const container = document.createElement("div");
|
||
container.style.cssText = "display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;";
|
||
const headerClone = header.cloneNode(true);
|
||
headerClone.style.margin = "0";
|
||
const btnGroup = document.createElement("div");
|
||
btnGroup.style.cssText = "display: flex; gap: 8px;";
|
||
btnGroup.appendChild(clearButton);
|
||
container.appendChild(headerClone);
|
||
container.appendChild(btnGroup);
|
||
header.parentElement.replaceChild(container, header);
|
||
}
|
||
} else {
|
||
clearButton.style.cssText += `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 999999;
|
||
padding: 12px 20px;
|
||
font-size: 14px;
|
||
`;
|
||
document.body.appendChild(clearButton);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 彻底清理浏览器数据 (Stripe 反指纹追踪)
|
||
*/
|
||
async function clearAllStripeData() {
|
||
try {
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
|
||
// 清理 IndexedDB
|
||
if (window.indexedDB) {
|
||
const dbs = await window.indexedDB.databases();
|
||
for (const db of dbs) {
|
||
window.indexedDB.deleteDatabase(db.name);
|
||
}
|
||
}
|
||
|
||
// 暴力清理 Cookies
|
||
const cookies = document.cookie.split(";");
|
||
for (let cookie of cookies) {
|
||
const eqPos = cookie.indexOf("=");
|
||
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + location.hostname;
|
||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=." + location.hostname;
|
||
}
|
||
|
||
if ("caches" in window) {
|
||
const keys = await caches.keys();
|
||
for (const key of keys) {
|
||
await caches.delete(key);
|
||
}
|
||
}
|
||
|
||
chrome.runtime.sendMessage({ action: "clearBrowsingData" });
|
||
console.log("[cardbingenerator] All Stripe data cleared");
|
||
} catch (err) {
|
||
console.error("Error clearing data:", err);
|
||
showNotification("⚠️ Partial clear - some data may remain", "warning");
|
||
}
|
||
}
|
||
|
||
function shouldShowButton() {
|
||
const hasCardFields = detectCardFields();
|
||
const hasAddrFields = detectAddressFields();
|
||
return hasCardFields || hasAddrFields.fullName || hasAddrFields.address1;
|
||
}
|
||
|
||
function initButton() {
|
||
if (shouldShowButton()) {
|
||
createFillButton();
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// 初始化与事件监听
|
||
// ==========================================
|
||
|
||
const observer = new MutationObserver(() => {
|
||
if (!fillButton && shouldShowButton()) {
|
||
createFillButton();
|
||
}
|
||
});
|
||
|
||
if (document.body) {
|
||
observer.observe(document.body, { childList: true, subtree: true });
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", initButton);
|
||
} else {
|
||
initButton();
|
||
}
|
||
|
||
// 多次尝试初始化,应对动态加载的 SPA
|
||
setTimeout(initButton, 1000);
|
||
setTimeout(initButton, 2000);
|
||
setTimeout(initButton, 3000);
|
||
|
||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||
if (request.action === "fillForm") {
|
||
autofillAll();
|
||
}
|
||
});
|
||
|
||
window.addEventListener("beforeunload", () => {
|
||
chrome.storage.local.remove(["generatedCards"]);
|
||
console.log("[cardbingenerator] Cleared cards on page unload");
|
||
});
|
||
|
||
chrome.storage.local.remove(["generatedCards"], () => {
|
||
console.log("[cardbingenerator] Cleared old cards on page load");
|
||
});
|