解混淆
This commit is contained in:
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# IDE and Editor Files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# OS Files
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Extension Build Files
|
||||||
|
*.zip
|
||||||
|
*.crx
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# Temporary Files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Node modules (if added in future)
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Test Files
|
||||||
|
test-results/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Private Keys and Certificates
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
secrets.json
|
||||||
|
|
||||||
|
# Environment Files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
145
CONTRIBUTING.md
Normal file
145
CONTRIBUTING.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Contributing to cardbingenerator - Stripe Auto Fill
|
||||||
|
|
||||||
|
First off, thank you for considering contributing to cardbingenerator! It's people like you that make cardbingenerator such a great tool.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code.
|
||||||
|
|
||||||
|
## How Can I Contribute?
|
||||||
|
|
||||||
|
### Reporting Bugs
|
||||||
|
|
||||||
|
Before creating bug reports, please check the existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible:
|
||||||
|
|
||||||
|
* **Use a clear and descriptive title**
|
||||||
|
* **Describe the exact steps which reproduce the problem**
|
||||||
|
* **Provide specific examples to demonstrate the steps**
|
||||||
|
* **Describe the behavior you observed after following the steps**
|
||||||
|
* **Explain which behavior you expected to see instead and why**
|
||||||
|
* **Include screenshots and animated GIFs** if possible
|
||||||
|
* **Include your browser version and OS**
|
||||||
|
|
||||||
|
### Suggesting Enhancements
|
||||||
|
|
||||||
|
Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include:
|
||||||
|
|
||||||
|
* **Use a clear and descriptive title**
|
||||||
|
* **Provide a step-by-step description of the suggested enhancement**
|
||||||
|
* **Provide specific examples to demonstrate the steps**
|
||||||
|
* **Describe the current behavior** and **explain which behavior you expected to see instead**
|
||||||
|
* **Explain why this enhancement would be useful**
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
* Fill in the required template
|
||||||
|
* Do not include issue numbers in the PR title
|
||||||
|
* Include screenshots and animated GIFs in your pull request whenever possible
|
||||||
|
* Follow the JavaScript style guide
|
||||||
|
* End all files with a newline
|
||||||
|
* Avoid platform-dependent code
|
||||||
|
|
||||||
|
## Development Process
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Clone your fork:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/cardbingenerator-extension.git
|
||||||
|
```
|
||||||
|
3. Create a branch:
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Making Changes
|
||||||
|
|
||||||
|
1. Make your changes
|
||||||
|
2. Test your changes thoroughly:
|
||||||
|
- Load extension in developer mode
|
||||||
|
- Test on multiple Stripe checkout pages
|
||||||
|
- Verify all functionality works
|
||||||
|
- Check console for errors
|
||||||
|
|
||||||
|
3. Commit your changes:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add: description of your changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Message Guidelines
|
||||||
|
|
||||||
|
* Use the present tense ("Add feature" not "Added feature")
|
||||||
|
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||||
|
* Limit the first line to 72 characters or less
|
||||||
|
* Reference issues and pull requests liberally after the first line
|
||||||
|
* Consider starting the commit message with an applicable emoji:
|
||||||
|
* 🎨 `:art:` when improving the format/structure of the code
|
||||||
|
* 🐛 `:bug:` when fixing a bug
|
||||||
|
* ✨ `:sparkles:` when adding a new feature
|
||||||
|
* 📝 `:memo:` when writing docs
|
||||||
|
* 🚀 `:rocket:` when improving performance
|
||||||
|
* 🔒 `:lock:` when dealing with security
|
||||||
|
* ⬆️ `:arrow_up:` when upgrading dependencies
|
||||||
|
* ⬇️ `:arrow_down:` when downgrading dependencies
|
||||||
|
* 🔧 `:wrench:` when changing configuration files
|
||||||
|
|
||||||
|
### Coding Style
|
||||||
|
|
||||||
|
* Use 2 spaces for indentation
|
||||||
|
* Use camelCase for variable and function names
|
||||||
|
* Use PascalCase for class names
|
||||||
|
* Use UPPER_CASE for constants
|
||||||
|
* Add comments for complex logic
|
||||||
|
* Keep functions small and focused
|
||||||
|
* Use meaningful variable names
|
||||||
|
* Avoid magic numbers
|
||||||
|
* Use template literals for string concatenation
|
||||||
|
|
||||||
|
### Testing Checklist
|
||||||
|
|
||||||
|
Before submitting your PR, ensure:
|
||||||
|
|
||||||
|
- [ ] Code follows the style guidelines
|
||||||
|
- [ ] No console errors
|
||||||
|
- [ ] Extension loads without errors
|
||||||
|
- [ ] All existing features still work
|
||||||
|
- [ ] New features work as expected
|
||||||
|
- [ ] Tested on multiple Stripe pages
|
||||||
|
- [ ] No performance degradation
|
||||||
|
- [ ] Code is commented where necessary
|
||||||
|
- [ ] No hardcoded values (use constants)
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
|
||||||
|
1. Update the README.md with details of changes if applicable
|
||||||
|
2. Update the CHANGELOG.md with a note describing your changes
|
||||||
|
3. The PR will be merged once you have the sign-off of a maintainer
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cardbingenerator-extension/
|
||||||
|
├── manifest.json # Extension configuration
|
||||||
|
├── popup.html # Popup UI
|
||||||
|
├── popup.js # Popup logic
|
||||||
|
├── styles.css # Styles
|
||||||
|
├── background.js # Service worker
|
||||||
|
├── content.js # Content script
|
||||||
|
├── icons/ # Extension icons
|
||||||
|
├── README.md # Documentation
|
||||||
|
├── LICENSE # License file
|
||||||
|
└── CONTRIBUTING.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Feel free to open an issue with your question or contact the maintainers directly.
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
Contributors will be recognized in the README.md file.
|
||||||
|
|
||||||
|
Thank you for contributing! 🎉
|
||||||
|
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 cardbingenerator - Stripe Auto Fill
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
546
README.md
Normal file
546
README.md
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# 💳 Cardbingenerator - Stripe Auto Fill
|
||||||
|
|
||||||
|
[](https://github.com/yourusername/cardbingenerator-extension)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://developer.chrome.com/docs/extensions/)
|
||||||
|
|
||||||
|
一个功能强大的 Chrome 浏览器扩展,用于自动生成测试信用卡号并自动填充 Stripe 支付表单。支持 Luhn 算法验证、自定义地址管理和现代化暗黑主题界面。
|
||||||
|
|
||||||
|
[English](#english-version) | [中文](#中文版本)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 中文版本
|
||||||
|
|
||||||
|
## ⚠️ 免责声明
|
||||||
|
|
||||||
|
**本工具仅用于测试和教育目的**
|
||||||
|
|
||||||
|
- 仅可用于开发/测试环境或演示支付表单
|
||||||
|
- 请勿在生产环境的支付系统中使用
|
||||||
|
- 请勿用于欺诈或任何非法活动
|
||||||
|
- 开发者对工具的滥用不承担任何责任
|
||||||
|
- 测试卡号无法在真实支付处理器上使用
|
||||||
|
|
||||||
|
## 🌟 核心特性
|
||||||
|
|
||||||
|
### 💳 智能卡号生成
|
||||||
|
- **Luhn 算法验证**:生成数学上有效的卡号
|
||||||
|
- **BIN 支持**:使用任意银行识别号码(BIN)模式
|
||||||
|
- **多卡种识别**:自动识别 Visa、Mastercard、Amex、Discover 等
|
||||||
|
- **智能生成**:包含有效的到期日期和 CVV 安全码
|
||||||
|
|
||||||
|
### 🚀 自动填充能力
|
||||||
|
- **智能字段检测**:自动查找并填充卡片字段
|
||||||
|
- **Shadow DOM 支持**:兼容现代 Web 组件
|
||||||
|
- **地址自动填充**:填充账单地址、城市、州、邮编
|
||||||
|
- **仿人类输入**:模拟自然的打字模式
|
||||||
|
- **框架兼容**:绕过 React/Vue 值设置器
|
||||||
|
|
||||||
|
### 🛠️ 高度定制化
|
||||||
|
- **自定义 BIN 历史**:保存并重用你喜欢的 BIN 模式
|
||||||
|
- **多地址管理**:管理自定义账单地址
|
||||||
|
- **姓名生成器**:内置身份池用于持卡人姓名
|
||||||
|
- **数据源选择**:在静态、手动或自动生成数据之间切换
|
||||||
|
|
||||||
|
### 🔒 隐私保护功能
|
||||||
|
- **数据清理**:深度清除 cookies、缓存和存储
|
||||||
|
- **反指纹追踪**:清除所有 Stripe 相关的浏览数据
|
||||||
|
- **隔离存储**:所有数据本地存储在浏览器中
|
||||||
|
|
||||||
|
## 📦 安装方法
|
||||||
|
|
||||||
|
### 从源码安装
|
||||||
|
|
||||||
|
1. **克隆仓库**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/cardbingenerator.git
|
||||||
|
cd cardbingenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在 Chrome 中加载扩展**
|
||||||
|
- 打开 Chrome 并访问 `chrome://extensions/`
|
||||||
|
- 启用"开发者模式"(右上角的开关)
|
||||||
|
- 点击"加载已解压的扩展程序"
|
||||||
|
- 选择项目文件夹
|
||||||
|
|
||||||
|
3. **固定扩展图标**
|
||||||
|
- 点击 Chrome 工具栏中的拼图图标
|
||||||
|
- 找到"cardbingenerator"并点击图钉图标
|
||||||
|
|
||||||
|
### 手动安装
|
||||||
|
|
||||||
|
1. 下载最新版本
|
||||||
|
2. 解压 ZIP 文件
|
||||||
|
3. 按照"从源码安装"中的步骤 2-3 操作
|
||||||
|
|
||||||
|
## 🚀 使用指南
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
1. **打开扩展**
|
||||||
|
- 点击 Chrome 工具栏中的扩展图标
|
||||||
|
- 你会看到主界面,包含标签页:General、BIN List、Settings
|
||||||
|
|
||||||
|
2. **输入 BIN 模式**
|
||||||
|
- 在"BIN Template"字段中输入 BIN(例如:`552461`)
|
||||||
|
- 扩展会自动用 'x' 填充到 16 位
|
||||||
|
- 示例:`552461` 会变成 `552461xxxxxxxxxx`
|
||||||
|
|
||||||
|
3. **生成并填充**
|
||||||
|
- 导航到 Stripe 结账页面
|
||||||
|
- 点击"Fill Everything"按钮
|
||||||
|
- 扩展将自动:
|
||||||
|
- 生成有效的卡号
|
||||||
|
- 填充卡片详情(卡号、到期日、CVV)
|
||||||
|
- 填充账单地址信息
|
||||||
|
- 自动提交表单
|
||||||
|
|
||||||
|
### 高级功能
|
||||||
|
|
||||||
|
#### BIN 历史记录
|
||||||
|
- 点击 BIN 输入框旁边的 `+` 按钮保存到历史
|
||||||
|
- 点击任意已保存的 BIN 快速选择
|
||||||
|
- 点击 `×` 从历史中删除
|
||||||
|
|
||||||
|
#### 自定义地址
|
||||||
|
1. 进入 **Settings** → **Addresses** 标签
|
||||||
|
2. 填写地址表单字段
|
||||||
|
3. 点击"Add"保存
|
||||||
|
4. 在 Address Source 下拉菜单中选择"Manual (Custom)"
|
||||||
|
|
||||||
|
#### 设置配置
|
||||||
|
|
||||||
|
**卡号验证开关**
|
||||||
|
- 启用:生成通过 Luhn 算法验证的卡号
|
||||||
|
- 禁用:生成随机卡号(更快,较宽松)
|
||||||
|
|
||||||
|
**地址来源**
|
||||||
|
- **Static**:使用内置默认地址
|
||||||
|
- **Manual**:使用你自定义保存的地址
|
||||||
|
- **Auto**:生成随机地址(即将推出)
|
||||||
|
|
||||||
|
**姓名来源**
|
||||||
|
- 控制是使用内置姓名还是自定义条目
|
||||||
|
- 姓名从地址条目中提取
|
||||||
|
|
||||||
|
## 🎯 工作原理
|
||||||
|
|
||||||
|
### 卡号生成算法
|
||||||
|
|
||||||
|
扩展使用 **Luhn 算法**(模 10 校验和)生成有效卡号:
|
||||||
|
|
||||||
|
1. 接收你的 BIN 模式(例如:`552461xxxxxxxxxx`)
|
||||||
|
2. 为每个 `x` 填充随机数字
|
||||||
|
3. 计算最后一位的 Luhn 校验位
|
||||||
|
4. 验证整个卡号
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 生成示例
|
||||||
|
BIN 输入: 552461xxxxxxxxxx
|
||||||
|
生成结果: 5524610123456789
|
||||||
|
验证状态: ✅ 通过 Luhn 验证
|
||||||
|
卡片类型: Mastercard
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动填充流程
|
||||||
|
|
||||||
|
1. **字段检测**:扫描页面查找卡片和地址字段
|
||||||
|
- 使用 CSS 选择器和 autocomplete 属性
|
||||||
|
- 穿透 Shadow DOM 边界
|
||||||
|
- 根据相关性为字段评分
|
||||||
|
|
||||||
|
2. **智能填充**:按最佳顺序填充字段
|
||||||
|
- 首先填充国家(可能会刷新表单结构)
|
||||||
|
- 使用模拟打字填充卡片详情
|
||||||
|
- 适当延迟填充地址字段
|
||||||
|
|
||||||
|
3. **事件触发**:触发适当的 DOM 事件
|
||||||
|
- 原生值设置器绕过框架锁定
|
||||||
|
- Input、change 和 blur 事件
|
||||||
|
- 焦点管理触发验证
|
||||||
|
|
||||||
|
## 🔧 技术细节
|
||||||
|
|
||||||
|
### 支持的 Stripe 版本
|
||||||
|
- Stripe Elements v3
|
||||||
|
- Stripe Checkout
|
||||||
|
- 自定义 Stripe 集成
|
||||||
|
|
||||||
|
### 浏览器兼容性
|
||||||
|
- Chrome 88+
|
||||||
|
- Edge 88+
|
||||||
|
- Brave(基于 Chromium)
|
||||||
|
- Opera(基于 Chromium)
|
||||||
|
|
||||||
|
### 使用的权限
|
||||||
|
|
||||||
|
| 权限 | 用途 |
|
||||||
|
|------------|---------|
|
||||||
|
| `storage` | 保存 BIN 历史和自定义地址 |
|
||||||
|
| `activeTab` | 与当前 Stripe 结账页面交互 |
|
||||||
|
| `tabs` | 查找和管理 Stripe 标签页 |
|
||||||
|
| `scripting` | 注入自动填充脚本 |
|
||||||
|
| `cookies` | 清除会话数据用于测试 |
|
||||||
|
| `browsingData` | 深度清除 Stripe 指纹数据 |
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
cardbingenerator/
|
||||||
|
├── manifest.json # 扩展配置
|
||||||
|
├── background.js # Service Worker(卡号生成逻辑)
|
||||||
|
├── content.js # Content Script(自动填充逻辑)
|
||||||
|
├── popup.html # 扩展用户界面
|
||||||
|
├── popup.js # UI 逻辑和设置
|
||||||
|
├── styles.css # 扩展样式
|
||||||
|
├── icon16.png # 扩展图标(16x16)
|
||||||
|
├── icon48.png # 扩展图标(48x48)
|
||||||
|
├── icon128.png # 扩展图标(128x128)
|
||||||
|
└── README.md # 本文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 支持的卡片类型
|
||||||
|
|
||||||
|
扩展自动检测并生成以下卡片:
|
||||||
|
|
||||||
|
- ✅ Visa(以 4 开头)
|
||||||
|
- ✅ Mastercard(以 51-55 开头)
|
||||||
|
- ✅ American Express(以 34、37 开头)
|
||||||
|
- ✅ Discover(以 6011、65 开头)
|
||||||
|
- ✅ JCB(以 35 开头)
|
||||||
|
- ✅ Diners Club(以 30、36、38 开头)
|
||||||
|
- ✅ Maestro(以 50、56-58、6304、6390、67 开头)
|
||||||
|
- ✅ UnionPay(以 62 开头)
|
||||||
|
|
||||||
|
## 🐛 故障排除
|
||||||
|
|
||||||
|
### 扩展未填充表单
|
||||||
|
- **解决方案**:确保你在 Stripe 结账页面上
|
||||||
|
- 检查页面是否已完全加载
|
||||||
|
- 尝试刷新页面并再次点击"Fill Everything"
|
||||||
|
|
||||||
|
### 生成的卡号被拒绝
|
||||||
|
- **解决方案**:在设置中启用"Card Validation"
|
||||||
|
- 某些测试环境需要 Luhn 有效的卡号
|
||||||
|
- 如果一个 BIN 不起作用,尝试使用不同的 BIN
|
||||||
|
|
||||||
|
### 字段填充不正确
|
||||||
|
- **解决方案**:页面可能有自定义字段名称
|
||||||
|
- 尝试点击"Clear All Data"按钮并刷新
|
||||||
|
- 报告问题时请提供页面 URL
|
||||||
|
|
||||||
|
### 扩展图标未出现
|
||||||
|
- **解决方案**:检查扩展是否在 `chrome://extensions/` 中启用
|
||||||
|
- 尝试重新加载扩展
|
||||||
|
- 检查浏览器控制台是否有错误
|
||||||
|
|
||||||
|
## 🔐 安全说明
|
||||||
|
|
||||||
|
### 仅本地存储
|
||||||
|
- 所有数据存储在 Chrome 本地存储中
|
||||||
|
- 敏感数据不调用外部 API
|
||||||
|
- 无遥测或跟踪
|
||||||
|
|
||||||
|
### 数据清理
|
||||||
|
使用"Clear All Data"按钮清除:
|
||||||
|
- Cookies(包括 HttpOnly)
|
||||||
|
- LocalStorage 和 SessionStorage
|
||||||
|
- IndexedDB 数据库
|
||||||
|
- 缓存和 Service Workers
|
||||||
|
- Stripe 指纹数据
|
||||||
|
|
||||||
|
## 📚 资源链接
|
||||||
|
|
||||||
|
- [Stripe 测试文档](https://stripe.com/docs/testing)
|
||||||
|
- [Luhn 算法详解](https://zh.wikipedia.org/wiki/Luhn%E7%AE%97%E6%B3%95)
|
||||||
|
- [Chrome 扩展开发](https://developer.chrome.com/docs/extensions/)
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
欢迎贡献!请遵循以下指南:
|
||||||
|
|
||||||
|
1. Fork 仓库
|
||||||
|
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. 提交更改 (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||||
|
5. 打开 Pull Request
|
||||||
|
|
||||||
|
### 开发设置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆你的 fork
|
||||||
|
git clone https://github.com/yourusername/cardbingenerator.git
|
||||||
|
|
||||||
|
# 创建分支
|
||||||
|
git checkout -b my-feature
|
||||||
|
|
||||||
|
# 进行更改并在 Chrome 中测试
|
||||||
|
# 从 chrome://extensions 加载未打包的扩展
|
||||||
|
|
||||||
|
# 提交并推送
|
||||||
|
git add .
|
||||||
|
git commit -m "你的功能描述"
|
||||||
|
git push origin my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
|
||||||
|
|
||||||
|
## ⚠️ 法律声明
|
||||||
|
|
||||||
|
本软件按"原样"提供,不提供任何形式的保证。作者和贡献者:
|
||||||
|
|
||||||
|
- 不支持欺诈活动
|
||||||
|
- 不鼓励绕过支付安全
|
||||||
|
- 对滥用不承担责任
|
||||||
|
- 建议仅在授权测试环境中使用
|
||||||
|
|
||||||
|
**使用风险和责任自负**
|
||||||
|
|
||||||
|
## 👨💻 作者
|
||||||
|
|
||||||
|
由 **Blackhat_bullet** 创建
|
||||||
|
|
||||||
|
- 📺 [YouTube 频道](https://www.youtube.com/@Blackhat_bullet)
|
||||||
|
- 💬 [Telegram 群组](https://t.me/+VJJt9csJoEUxZTA1)
|
||||||
|
- 📞 [临时短信服务](https://t.me/Tempotpsms_bot)
|
||||||
|
- 💳 [BIN 生成器网站](https://cardbingenerator.com/)
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
- Stripe 提供的优秀测试文档
|
||||||
|
- Chrome 扩展社区
|
||||||
|
- 所有贡献者和测试人员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<strong>记住:本工具仅用于测试目的。请始终尊重法律和你正在测试的平台的服务条款。</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English Version
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
**This tool is for TESTING and EDUCATIONAL purposes only.**
|
||||||
|
|
||||||
|
- Use only on development/test environments or demo payment forms
|
||||||
|
- Do NOT use on production payment systems
|
||||||
|
- Do NOT use for fraud or any illegal activities
|
||||||
|
- The developers assume NO responsibility for misuse of this tool
|
||||||
|
- Test cards will not work on real payment processors
|
||||||
|
|
||||||
|
## 🌟 Features
|
||||||
|
|
||||||
|
### 💳 Smart Card Generation
|
||||||
|
- **Luhn Algorithm Validation**: Generates mathematically valid card numbers
|
||||||
|
- **BIN Support**: Use any Bank Identification Number (BIN) pattern
|
||||||
|
- **Multiple Card Types**: Automatically detects Visa, Mastercard, Amex, Discover, etc.
|
||||||
|
- **Smart Generation**: Includes valid expiry dates and CVV codes
|
||||||
|
|
||||||
|
### 🚀 Auto-Fill Capabilities
|
||||||
|
- **Intelligent Field Detection**: Automatically finds and fills card fields
|
||||||
|
- **Shadow DOM Support**: Works with modern web components
|
||||||
|
- **Address Auto-Fill**: Fills billing address, city, state, ZIP code
|
||||||
|
- **Human-Like Input**: Simulates natural typing patterns
|
||||||
|
- **Framework Compatible**: Bypasses React/Vue value setters
|
||||||
|
|
||||||
|
### 🛠️ Customization
|
||||||
|
- **Custom BIN History**: Save and reuse your favorite BIN patterns
|
||||||
|
- **Multiple Addresses**: Manage custom billing addresses
|
||||||
|
- **Name Generator**: Built-in identity pool for cardholder names
|
||||||
|
- **Source Selection**: Choose between static, manual, or auto-generated data
|
||||||
|
|
||||||
|
### 🔒 Privacy Features
|
||||||
|
- **Data Clearing**: Deep clean cookies, cache, and storage
|
||||||
|
- **Anti-Fingerprinting**: Clear all Stripe-related browsing data
|
||||||
|
- **Isolated Storage**: All data stored locally in your browser
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/cardbingenerator.git
|
||||||
|
cd cardbingenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the extension in Chrome**
|
||||||
|
- Open Chrome and go to `chrome://extensions/`
|
||||||
|
- Enable "Developer mode" (toggle in top right)
|
||||||
|
- Click "Load unpacked"
|
||||||
|
- Select the project folder
|
||||||
|
|
||||||
|
3. **Pin the extension**
|
||||||
|
- Click the puzzle icon in Chrome toolbar
|
||||||
|
- Find "cardbingenerator" and click the pin icon
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
1. **Open the Extension**
|
||||||
|
- Click the extension icon in your Chrome toolbar
|
||||||
|
- You'll see the main interface with tabs: General, BIN List, Settings
|
||||||
|
|
||||||
|
2. **Enter a BIN Pattern**
|
||||||
|
- In the "BIN Template" field, enter a BIN (e.g., `552461`)
|
||||||
|
- The extension auto-fills with 'x' to create a 16-digit pattern
|
||||||
|
- Example: `552461` becomes `552461xxxxxxxxxx`
|
||||||
|
|
||||||
|
3. **Generate & Fill**
|
||||||
|
- Navigate to a Stripe checkout page
|
||||||
|
- Click "Fill Everything" button
|
||||||
|
- The extension will:
|
||||||
|
- Generate a valid card number
|
||||||
|
- Fill card details (number, expiry, CVV)
|
||||||
|
- Fill billing address information
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
#### BIN History
|
||||||
|
- Click the `+` button next to BIN input to save to history
|
||||||
|
- Click any saved BIN to quickly select it
|
||||||
|
- Click `×` to remove from history
|
||||||
|
|
||||||
|
#### Custom Addresses
|
||||||
|
1. Go to **Settings** → **Addresses** tab
|
||||||
|
2. Fill in the address form fields
|
||||||
|
3. Click "Add" to save
|
||||||
|
4. Select "Manual (Custom)" in the Address Source dropdown
|
||||||
|
|
||||||
|
#### Settings Configuration
|
||||||
|
|
||||||
|
**Card Validation Toggle**
|
||||||
|
- Enable: Generates cards that pass Luhn algorithm validation
|
||||||
|
- Disable: Generates random cards (faster, less strict)
|
||||||
|
|
||||||
|
**Address Source**
|
||||||
|
- **Static**: Use built-in default addresses
|
||||||
|
- **Manual**: Use your custom saved addresses
|
||||||
|
- **Auto**: Generate random addresses (coming soon)
|
||||||
|
|
||||||
|
**Name Source**
|
||||||
|
- Controls whether to use built-in names or custom entries
|
||||||
|
- Names are taken from the address entries
|
||||||
|
|
||||||
|
## 🎯 How It Works
|
||||||
|
|
||||||
|
### Card Number Generation
|
||||||
|
|
||||||
|
The extension uses the **Luhn Algorithm** (mod-10 checksum) to generate valid card numbers:
|
||||||
|
|
||||||
|
1. Takes your BIN pattern (e.g., `552461xxxxxxxxxx`)
|
||||||
|
2. Fills random digits for each `x`
|
||||||
|
3. Calculates the Luhn check digit for the last position
|
||||||
|
4. Validates the entire card number
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Example generated card
|
||||||
|
BIN Input: 552461xxxxxxxxxx
|
||||||
|
Generated: 5524610123456789
|
||||||
|
Valid: ✅ Passes Luhn validation
|
||||||
|
Type: Mastercard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Fill Process
|
||||||
|
|
||||||
|
1. **Field Detection**: Scans the page for card and address fields
|
||||||
|
- Uses CSS selectors and autocomplete attributes
|
||||||
|
- Penetrates Shadow DOM boundaries
|
||||||
|
- Scores fields by relevance
|
||||||
|
|
||||||
|
2. **Smart Filling**: Fills fields in optimal order
|
||||||
|
- Country first (may refresh form structure)
|
||||||
|
- Card details with simulated typing
|
||||||
|
- Address fields with proper delays
|
||||||
|
|
||||||
|
3. **Event Triggering**: Fires proper DOM events
|
||||||
|
- Native value setters bypass framework locks
|
||||||
|
- Input, change, and blur events
|
||||||
|
- Focus management for validation triggers
|
||||||
|
|
||||||
|
## 🎨 Supported Card Types
|
||||||
|
|
||||||
|
The extension automatically detects and generates cards for:
|
||||||
|
|
||||||
|
- ✅ Visa (starts with 4)
|
||||||
|
- ✅ Mastercard (starts with 51-55)
|
||||||
|
- ✅ American Express (starts with 34, 37)
|
||||||
|
- ✅ Discover (starts with 6011, 65)
|
||||||
|
- ✅ JCB (starts with 35)
|
||||||
|
- ✅ Diners Club (starts with 30, 36, 38)
|
||||||
|
- ✅ Maestro (starts with 50, 56-58, 6304, 6390, 67)
|
||||||
|
- ✅ UnionPay (starts with 62)
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Extension doesn't fill the form
|
||||||
|
- **Solution**: Make sure you're on a Stripe checkout page
|
||||||
|
- Check that the page has loaded completely
|
||||||
|
- Try refreshing the page and clicking "Fill Everything" again
|
||||||
|
|
||||||
|
### Generated cards are rejected
|
||||||
|
- **Solution**: Enable "Card Validation" in settings
|
||||||
|
- Some test environments require Luhn-valid cards
|
||||||
|
- Use a different BIN if one doesn't work
|
||||||
|
|
||||||
|
### Fields are filled incorrectly
|
||||||
|
- **Solution**: The page may have custom field names
|
||||||
|
- Try clicking the "Clear All Data" button and refresh
|
||||||
|
- Report the issue with the page URL
|
||||||
|
|
||||||
|
### Extension icon doesn't appear
|
||||||
|
- **Solution**: Check if extension is enabled in `chrome://extensions/`
|
||||||
|
- Try reloading the extension
|
||||||
|
- Check browser console for errors
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
### Local Storage Only
|
||||||
|
- All data stored locally in Chrome storage
|
||||||
|
- No external API calls for sensitive data
|
||||||
|
- No telemetry or tracking
|
||||||
|
|
||||||
|
### Data Clearing
|
||||||
|
Use the "Clear All Data" button to remove:
|
||||||
|
- Cookies (including HttpOnly)
|
||||||
|
- LocalStorage and SessionStorage
|
||||||
|
- IndexedDB databases
|
||||||
|
- Cache and Service Workers
|
||||||
|
- Stripe fingerprinting data
|
||||||
|
|
||||||
|
## 👨💻 Author
|
||||||
|
|
||||||
|
Created by **Blackhat_bullet**
|
||||||
|
|
||||||
|
- 📺 [YouTube Channel](https://www.youtube.com/@Blackhat_bullet)
|
||||||
|
- 💬 [Telegram Group](https://t.me/+VJJt9csJoEUxZTA1)
|
||||||
|
- 📞 [Temp SMS Service](https://t.me/Tempotpsms_bot)
|
||||||
|
- 💳 [BIN Generator Website](https://cardbingenerator.com/)
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## ⚠️ Legal Notice
|
||||||
|
|
||||||
|
This software is provided "as is" without warranty of any kind. The authors and contributors:
|
||||||
|
|
||||||
|
- Do NOT endorse fraudulent activities
|
||||||
|
- Do NOT encourage bypassing payment security
|
||||||
|
- Do NOT take responsibility for misuse
|
||||||
|
- Recommend using only in authorized test environments
|
||||||
|
|
||||||
|
**Use at your own risk and responsibility.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<strong>Remember: This tool is for testing purposes only. Always respect the law and terms of service of the platforms you're testing on.</strong>
|
||||||
|
</div>
|
||||||
550
background.js
Normal file
550
background.js
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
/**
|
||||||
|
* 虚拟身份池
|
||||||
|
* 用于生成随机持卡人姓名
|
||||||
|
*/
|
||||||
|
const FIRST_NAMES = ["John", "Michael", "David", "James", "Robert", "William", "Richard", "Joseph", "Charles", "Thomas", "Christopher", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Elizabeth", "Susan", "Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra"];
|
||||||
|
const LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认地址配置
|
||||||
|
*/
|
||||||
|
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"
|
||||||
|
}];
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 算法工具函数 (Luhn Algorithm)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function randomChoice(array) {
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据卡号前缀识别卡组织类型
|
||||||
|
*/
|
||||||
|
function getCardType(cardNumber) {
|
||||||
|
const patterns = {
|
||||||
|
Visa: /^4/,
|
||||||
|
Mastercard: /^5[1-5]/,
|
||||||
|
"American Express": /^3[47]/,
|
||||||
|
Discover: /^6(?:011|5)/,
|
||||||
|
JCB: /^35/,
|
||||||
|
"Diners Club": /^3(?:0[0-5]|[68])/,
|
||||||
|
Maestro: /^(?:5[0678]\d\d|6304|6390|67\d\d)/,
|
||||||
|
UnionPay: /^62/
|
||||||
|
};
|
||||||
|
for (const [type, regex] of Object.entries(patterns)) {
|
||||||
|
if (regex.test(cardNumber)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算 Luhn 校验位 (模10算法)
|
||||||
|
* 用于确保生成的卡号在数学上是有效的
|
||||||
|
*/
|
||||||
|
function calculateLuhnCheckDigit(digits) {
|
||||||
|
let sum = 0;
|
||||||
|
let shouldDouble = true;
|
||||||
|
for (let i = digits.length - 1; i >= 0; i--) {
|
||||||
|
let digit = parseInt(digits[i]);
|
||||||
|
if (shouldDouble) {
|
||||||
|
digit *= 2;
|
||||||
|
if (digit > 9) {
|
||||||
|
digit -= 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum += digit;
|
||||||
|
shouldDouble = !shouldDouble;
|
||||||
|
}
|
||||||
|
return (10 - sum % 10) % 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证卡号是否符合 Luhn 算法
|
||||||
|
*/
|
||||||
|
function validateLuhn(number) {
|
||||||
|
const digits = number.replace(/\D/g, "");
|
||||||
|
let sum = 0;
|
||||||
|
let shouldDouble = false;
|
||||||
|
for (let i = digits.length - 1; i >= 0; i--) {
|
||||||
|
let digit = parseInt(digits[i]);
|
||||||
|
if (shouldDouble) {
|
||||||
|
digit *= 2;
|
||||||
|
if (digit > 9) {
|
||||||
|
digit -= 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum += digit;
|
||||||
|
shouldDouble = !shouldDouble;
|
||||||
|
}
|
||||||
|
return sum % 10 === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成符合校验规则的卡号
|
||||||
|
* @param {string} binPattern - BIN 格式 (如 "552461xxxxxxxxxx")
|
||||||
|
*/
|
||||||
|
function generateValidCardNumber(binPattern) {
|
||||||
|
let rawNumber = "";
|
||||||
|
// 1. 填充随机数
|
||||||
|
for (let i = 0; i < binPattern.length - 1; i++) {
|
||||||
|
if (binPattern[i] === "x" || binPattern[i] === "X") {
|
||||||
|
rawNumber += Math.floor(Math.random() * 10);
|
||||||
|
} else {
|
||||||
|
rawNumber += binPattern[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 计算最后一位校验码
|
||||||
|
const checkDigit = calculateLuhnCheckDigit(rawNumber);
|
||||||
|
rawNumber += checkDigit;
|
||||||
|
return rawNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateExpiryDate() {
|
||||||
|
const now = new Date();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const currentMonth = now.getMonth() + 1;
|
||||||
|
|
||||||
|
// 随机生成 1 到 60 个月后的日期
|
||||||
|
const randomMonths = Math.floor(Math.random() * 60) + 1;
|
||||||
|
let targetMonth = currentMonth + randomMonths;
|
||||||
|
let targetYear = currentYear;
|
||||||
|
|
||||||
|
while (targetMonth > 12) {
|
||||||
|
targetMonth -= 12;
|
||||||
|
targetYear += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
month: targetMonth.toString().padStart(2, "0"),
|
||||||
|
year: targetYear.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCVV(length = 3) {
|
||||||
|
let cvv = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
cvv += Math.floor(Math.random() * 10);
|
||||||
|
}
|
||||||
|
return cvv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 核心生成逻辑
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地生成模式:使用内置算法生成卡号
|
||||||
|
*/
|
||||||
|
function generateCardsLocally(bin, amount = 10) {
|
||||||
|
const cards = [];
|
||||||
|
const uniqueSet = new Set();
|
||||||
|
console.log(`🎲 Generating ${amount} valid cards from BIN: ${bin}`);
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = amount * 10;
|
||||||
|
|
||||||
|
while (cards.length < amount && attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
const number = generateValidCardNumber(bin);
|
||||||
|
|
||||||
|
if (uniqueSet.has(number)) continue;
|
||||||
|
if (!validateLuhn(number)) {
|
||||||
|
console.warn("⚠️ Generated invalid card (should not happen):", number);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueSet.add(number);
|
||||||
|
const expiry = generateExpiryDate();
|
||||||
|
const cvv = generateCVV(3);
|
||||||
|
const type = getCardType(number);
|
||||||
|
|
||||||
|
cards.push({
|
||||||
|
serial_number: cards.length + 1,
|
||||||
|
card_number: number,
|
||||||
|
expiry_month: expiry.month,
|
||||||
|
expiry_year: expiry.year,
|
||||||
|
cvv: cvv,
|
||||||
|
card_type: type,
|
||||||
|
full_format: `${number}|${expiry.month}|${expiry.year}|${cvv}`,
|
||||||
|
luhn_valid: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[cardbingenerator] Successfully generated ${cards.length} valid cards`);
|
||||||
|
|
||||||
|
// 最后的完整性检查
|
||||||
|
const invalid = cards.filter(c => !validateLuhn(c.card_number));
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
console.error(`❌ Found ${invalid.length} invalid cards!`);
|
||||||
|
} else {
|
||||||
|
console.log("[cardbingenerator] All cards passed Luhn validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计卡种分布
|
||||||
|
const stats = {};
|
||||||
|
cards.forEach(c => {
|
||||||
|
stats[c.card_type] = (stats[c.card_type] || 0) + 1;
|
||||||
|
});
|
||||||
|
console.log("📊 Card types:", stats);
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单生成模式:仅生成随机数,不进行 Luhn 校验 (用于某些不需要校验的测试场景)
|
||||||
|
*/
|
||||||
|
function generateCardsSimple(bin, amount = 10) {
|
||||||
|
const cards = [];
|
||||||
|
const uniqueSet = new Set();
|
||||||
|
console.log(`🎲 Generating ${amount} cards (no validation) from BIN: ${bin}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
let number = "";
|
||||||
|
for (let j = 0; j < bin.length; j++) {
|
||||||
|
if (bin[j] === "x" || bin[j] === "X") {
|
||||||
|
number += Math.floor(Math.random() * 10);
|
||||||
|
} else {
|
||||||
|
number += bin[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSet.has(number)) {
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uniqueSet.add(number);
|
||||||
|
const expiry = generateExpiryDate();
|
||||||
|
const cvv = generateCVV(3);
|
||||||
|
const type = getCardType(number);
|
||||||
|
|
||||||
|
cards.push({
|
||||||
|
serial_number: i + 1,
|
||||||
|
card_number: number,
|
||||||
|
expiry_month: expiry.month,
|
||||||
|
expiry_year: expiry.year,
|
||||||
|
cvv: cvv,
|
||||||
|
card_type: type,
|
||||||
|
full_format: `${number}|${expiry.month}|${expiry.year}|${cvv}`,
|
||||||
|
luhn_valid: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`[cardbingenerator] Generated ${cards.length} cards (simple mode)`);
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 远程生成逻辑 (AKR-Gen)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程生成模式:操控第三方网站 (akr-gen.bigfk.com) 生成卡号
|
||||||
|
* 当本地算法不满足需求时使用
|
||||||
|
*/
|
||||||
|
async function generateCardsFromAKR(bin, onComplete) {
|
||||||
|
let tab = null;
|
||||||
|
try {
|
||||||
|
console.log("[cardbingenerator] Opening AKR-gen tab...");
|
||||||
|
// 创建一个不激活的标签页(后台静默打开)
|
||||||
|
tab = await chrome.tabs.create({
|
||||||
|
url: "https://akr-gen.bigfk.com/",
|
||||||
|
active: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[cardbingenerator] Waiting for page load...");
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||||
|
|
||||||
|
console.log("[cardbingenerator] Filling BIN and generating cards...");
|
||||||
|
// 注入脚本:自动填入BIN并点击生成
|
||||||
|
const fillResult = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
func: remotePage_fillAndClick,
|
||||||
|
args: [bin]
|
||||||
|
});
|
||||||
|
console.log("Fill result:", fillResult[0]?.result);
|
||||||
|
|
||||||
|
console.log("⏳ Waiting a moment before checking results...");
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
console.log("📥 Getting generated cards (will wait up to 10 seconds)...");
|
||||||
|
// 注入脚本:抓取结果
|
||||||
|
const scrapeResult = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
func: remotePage_scrapeResults
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[cardbingenerator] Closing AKR-gen tab...");
|
||||||
|
await chrome.tabs.remove(tab.id);
|
||||||
|
tab = null;
|
||||||
|
|
||||||
|
if (scrapeResult && scrapeResult[0] && scrapeResult[0].result) {
|
||||||
|
const parsedCards = parseScrapedCards(scrapeResult[0].result);
|
||||||
|
console.log(`[cardbingenerator] Generated ${parsedCards.length} cards`);
|
||||||
|
|
||||||
|
if (parsedCards.length > 0) {
|
||||||
|
const randomAddr = await getRandomAddress();
|
||||||
|
// 存入 storage 供 Content Script 使用
|
||||||
|
chrome.storage.local.set({
|
||||||
|
generatedCards: parsedCards,
|
||||||
|
randomData: randomAddr
|
||||||
|
});
|
||||||
|
onComplete({ success: true, cards: parsedCards });
|
||||||
|
} else {
|
||||||
|
console.error("❌ No cards generated from AKR");
|
||||||
|
onComplete({ success: false, error: "No cards generated from AKR-gen" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("❌ Failed to retrieve cards from result");
|
||||||
|
onComplete({ success: false, error: "Failed to retrieve cards from page" });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error in generateCardsFromAKR:", err);
|
||||||
|
if (tab) {
|
||||||
|
try { await chrome.tabs.remove(tab.id); } catch (e) {}
|
||||||
|
}
|
||||||
|
onComplete({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 以下函数会被注入到目标页面执行,不能使用外部变量 --
|
||||||
|
|
||||||
|
function remotePage_fillAndClick(bin) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
function waitForElement(selector, retries = 10, delay = 300) {
|
||||||
|
return new Promise(res => {
|
||||||
|
let count = 0;
|
||||||
|
const check = () => {
|
||||||
|
const el = document.querySelector(selector) || document.getElementById(selector.replace("#", ""));
|
||||||
|
if (el) {
|
||||||
|
res(el);
|
||||||
|
} else if (count < retries) {
|
||||||
|
count++;
|
||||||
|
setTimeout(check, delay);
|
||||||
|
} else {
|
||||||
|
res(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForElement("bin").then(input => {
|
||||||
|
if (input) {
|
||||||
|
console.log("[cardbingenerator] Found BIN input, filling with:", bin);
|
||||||
|
input.value = bin;
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
waitForElement("button[type=\"submit\"]").then(btn => {
|
||||||
|
if (btn) {
|
||||||
|
console.log("[cardbingenerator] Found generate button, clicking...");
|
||||||
|
btn.click();
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
console.error("❌ Generate button not found");
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
console.error("❌ BIN input not found");
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function remotePage_scrapeResults() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
function waitLoop(retries = 20, delay = 500) {
|
||||||
|
let count = 0;
|
||||||
|
const check = () => {
|
||||||
|
const resultArea = document.getElementById("result");
|
||||||
|
if (resultArea && resultArea.value.trim()) {
|
||||||
|
console.log("[cardbingenerator] Found generated cards:", resultArea.value.split("\n").length, "lines");
|
||||||
|
resolve(resultArea.value);
|
||||||
|
} else if (count < retries) {
|
||||||
|
count++;
|
||||||
|
console.log(`[cardbingenerator] Waiting for cards... attempt ${count}/${retries}`);
|
||||||
|
setTimeout(check, delay);
|
||||||
|
} else {
|
||||||
|
console.error("❌ Timeout waiting for cards");
|
||||||
|
resolve("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
waitLoop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
function parseScrapedCards(text) {
|
||||||
|
if (!text) return [];
|
||||||
|
const lines = text.trim().split("\n");
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line.trim()) {
|
||||||
|
const parts = line.trim().split("|");
|
||||||
|
if (parts.length === 4) {
|
||||||
|
cards.push({
|
||||||
|
serial_number: index + 1,
|
||||||
|
card_number: parts[0],
|
||||||
|
expiry_month: parts[1],
|
||||||
|
expiry_year: parts[2],
|
||||||
|
cvv: parts[3],
|
||||||
|
full_format: line.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助:获取随机地址
|
||||||
|
*/
|
||||||
|
async function getRandomAddress() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.local.get(["customAddresses"], data => {
|
||||||
|
const custom = data.customAddresses || [];
|
||||||
|
const pool = [...custom, ...DEFAULT_ADDRESSES];
|
||||||
|
if (pool.length === 0) {
|
||||||
|
resolve(DEFAULT_ADDRESSES[0]);
|
||||||
|
} else {
|
||||||
|
const selected = randomChoice(pool);
|
||||||
|
resolve(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 数据清理逻辑 (Browsing Data API)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async function clearStripeBrowsingData(sendResponse) {
|
||||||
|
try {
|
||||||
|
const domains = ["stripe.com", "checkout.stripe.com", "js.stripe.com", "hooks.stripe.com"];
|
||||||
|
|
||||||
|
// 1. 深度清理 Cookies (针对特定域)
|
||||||
|
for (const domain of domains) {
|
||||||
|
const cookies = await chrome.cookies.getAll({ domain: domain });
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
await chrome.cookies.remove({
|
||||||
|
url: "https://" + cookie.domain + cookie.path,
|
||||||
|
name: cookie.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 清理浏览器缓存和存储 (针对特定 Origin)
|
||||||
|
await chrome.browsingData.remove({
|
||||||
|
origins: domains.map(d => "https://" + d)
|
||||||
|
}, {
|
||||||
|
cache: true,
|
||||||
|
cookies: true,
|
||||||
|
localStorage: true,
|
||||||
|
indexedDB: true,
|
||||||
|
serviceWorkers: true,
|
||||||
|
cacheStorage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[cardbingenerator] Deep clear completed for Stripe domains");
|
||||||
|
if (sendResponse) sendResponse({ success: true });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in deep clear:", err);
|
||||||
|
if (sendResponse) sendResponse({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 消息监听与路由
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 安装时初始化默认 BIN
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.storage.local.get(["currentBin", "binHistory"], data => {
|
||||||
|
if (!data.currentBin) {
|
||||||
|
chrome.storage.local.set({
|
||||||
|
currentBin: "552461xxxxxxxxxx",
|
||||||
|
binHistory: ["552461xxxxxxxxxx"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 主消息处理器
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === "generateCards") {
|
||||||
|
// 路由到生成逻辑
|
||||||
|
generateCardsHandler(request.bin, request.useValidation, sendResponse);
|
||||||
|
return true; // 保持消息通道打开以进行异步响应
|
||||||
|
}
|
||||||
|
if (request.action === "clearBrowsingData") {
|
||||||
|
// 路由到清理逻辑
|
||||||
|
clearStripeBrowsingData(sendResponse);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function generateCardsHandler(bin, useValidation = true, sendResponse) {
|
||||||
|
try {
|
||||||
|
console.log(`[cardbingenerator] Starting card generation... (Luhn: ${useValidation ? "ON" : "OFF"})`);
|
||||||
|
|
||||||
|
// 根据配置决定是验证 Luhn 还是简单生成
|
||||||
|
const cards = useValidation ?
|
||||||
|
generateCardsLocally(bin, 10) :
|
||||||
|
generateCardsSimple(bin, 10);
|
||||||
|
|
||||||
|
if (cards.length > 0) {
|
||||||
|
const address = await getRandomAddress();
|
||||||
|
|
||||||
|
// 将结果存入 storage,这样 Content Script (第一部分代码) 就能读到了
|
||||||
|
chrome.storage.local.set({
|
||||||
|
generatedCards: cards,
|
||||||
|
randomData: address
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[cardbingenerator] Generated and saved ${cards.length} cards`);
|
||||||
|
sendResponse({ success: true, cards: cards });
|
||||||
|
} else {
|
||||||
|
console.error("❌ No cards generated");
|
||||||
|
sendResponse({ success: false, error: "Failed to generate cards" });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error in generateCardsHandler:", err);
|
||||||
|
sendResponse({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
880
content.js
Normal file
880
content.js
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
/**
|
||||||
|
* 全局状态标记
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
});
|
||||||
BIN
icon128.png
Normal file
BIN
icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
icon16.png
Normal file
BIN
icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 703 B |
BIN
icon48.png
Normal file
BIN
icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
43
manifest.json
Normal file
43
manifest.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "cardbingenerator - Stripe Auto Fill",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "cardbingenerator",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
|
"tabs",
|
||||||
|
"scripting",
|
||||||
|
"cookies",
|
||||||
|
"browsingData"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://checkout.stripe.com/*",
|
||||||
|
"https://stripe.com/*",
|
||||||
|
"https://*.stripe.com/*"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icon16.png",
|
||||||
|
"48": "icon48.png",
|
||||||
|
"128": "icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://checkout.stripe.com/*",
|
||||||
|
"https://stripe.com/*",
|
||||||
|
"https://*.stripe.com/*"
|
||||||
|
],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_idle",
|
||||||
|
"all_frames": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
249
popup.html
Normal file
249
popup.html
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>cardbingenerator</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Telegram Promotion Modal -->
|
||||||
|
<div id="telegramModal" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="modal-close" id="modalCloseBtn">×</button>
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-icon">📱</div>
|
||||||
|
<h3 class="modal-title">Join Our Community</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-description">Get access to more hacks, tools, and exclusive content!</p>
|
||||||
|
<div class="modal-links">
|
||||||
|
<a href="https://t.me/+VJJt9csJoEUxZTA1" target="_blank" class="modal-link telegram-link">
|
||||||
|
<span class="modal-link-icon">💬</span>
|
||||||
|
<div class="modal-link-content">
|
||||||
|
<div class="modal-link-title">Join Telegram Group</div>
|
||||||
|
<div class="modal-link-subtitle">More Hacks & Tools</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/Tempotpsms_bot" target="_blank" class="modal-link temp-link">
|
||||||
|
<span class="modal-link-icon">📞</span>
|
||||||
|
<div class="modal-link-content">
|
||||||
|
<div class="modal-link-title">Temp Number Service</div>
|
||||||
|
<div class="modal-link-subtitle">Virtual SMS Bot</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-timer">
|
||||||
|
<span id="modalTimer">5</span>s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="logo">💳</div>
|
||||||
|
<h2>cardbingenerator</h2>
|
||||||
|
</div>
|
||||||
|
<div class="branding">
|
||||||
|
<div class="brand-name">Blackhat_bullet</div>
|
||||||
|
<div class="brand-links">
|
||||||
|
<a href="https://www.youtube.com/@Blackhat_bullet" target="_blank" class="brand-link">YouTube</a>
|
||||||
|
<a href="https://cardbingenerator.com/" target="_blank" class="brand-link">BIN Site</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="general">
|
||||||
|
<span class="tab-icon">⚡</span>
|
||||||
|
<span>General</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="binlist">
|
||||||
|
<span class="tab-icon">💎</span>
|
||||||
|
<span>BIN List</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="settings">
|
||||||
|
<span class="tab-icon">⚙️</span>
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- General Tab -->
|
||||||
|
<div id="general-tab" class="tab-content active">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-icon">💳</span>
|
||||||
|
<h3>Card Generator</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="binInput">BIN Template:</label>
|
||||||
|
<div class="input-with-btn">
|
||||||
|
<input type="text" id="binInput" placeholder="Enter BIN (e.g. 552461)" maxlength="19">
|
||||||
|
<button id="addBinBtn" class="icon-btn" title="Save to history">
|
||||||
|
<span>+</span>
|
||||||
|
</button>
|
||||||
|
<button id="settingsToggleBtn" class="icon-btn settings-btn" title="Settings">
|
||||||
|
<span>⚙️</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mini settings dropdown -->
|
||||||
|
<div id="miniSettings" class="mini-settings">
|
||||||
|
<div class="mini-settings-item">
|
||||||
|
<label class="mini-checkbox-label">
|
||||||
|
<input type="checkbox" id="useLuhnValidation" checked>
|
||||||
|
<span class="mini-checkbox-custom"></span>
|
||||||
|
<span class="mini-checkbox-text">Card Validation</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-settings-divider"></div>
|
||||||
|
|
||||||
|
<div class="mini-settings-item">
|
||||||
|
<div class="mini-settings-label">🏠 Address Source:</div>
|
||||||
|
<select id="addressSourceSelect" class="mini-select">
|
||||||
|
<option value="static">Static (Built-in)</option>
|
||||||
|
<option value="manual">Manual (Custom)</option>
|
||||||
|
<option value="auto" disabled>Auto (Coming Soon)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-settings-item">
|
||||||
|
<div class="mini-settings-label">👤 Name Source:</div>
|
||||||
|
<select id="nameSourceSelect" class="mini-select">
|
||||||
|
<option value="static">Static (Built-in)</option>
|
||||||
|
<option value="manual">Manual (Custom)</option>
|
||||||
|
<option value="auto" disabled>Auto (Coming Soon)</option>
|
||||||
|
</select>
|
||||||
|
<div class="mini-settings-hint">Names are taken from addresses</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<small style="color: var(--text-muted); font-size: 12px; margin-top: 4px; display: block;">
|
||||||
|
Auto-fills with 'x' to 16 digits • Max 19 digits for long cards
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="generateCardsBtn" class="btn btn-primary">
|
||||||
|
<span class="btn-icon">🚀</span>
|
||||||
|
<span>Fill Everything</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="status-message" id="statusMessage"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-icon">📜</span>
|
||||||
|
<h3>History</h3>
|
||||||
|
</div>
|
||||||
|
<div id="binHistoryList" class="history-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BIN List Tab -->
|
||||||
|
<div id="binlist-tab" class="tab-content">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-icon">💎</span>
|
||||||
|
<h3>BIN Generator</h3>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; gap: 20px;">
|
||||||
|
<div style="font-size: 64px; opacity: 0.4;">💎</div>
|
||||||
|
<div style="font-size: 24px; font-weight: 700; color: var(--text-primary); text-align: center;">
|
||||||
|
Powered by cardbingenerator
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px; color: var(--text-secondary); text-align: center; max-width: 320px; line-height: 1.6;">
|
||||||
|
Visit <a href="https://cardbingenerator.com/" target="_blank" style="color: var(--accent-primary); text-decoration: none; font-weight: 600;">cardbingenerator.com</a> for advanced BIN generation with multiple card networks, formats, and validation options.
|
||||||
|
</div>
|
||||||
|
<a href="https://cardbingenerator.com/" target="_blank" class="btn btn-primary" style="margin-top: 10px; text-decoration: none; display: inline-flex; width: auto; padding: 12px 24px;">
|
||||||
|
<span>Visit BIN Generator</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-branding">
|
||||||
|
<div class="footer-branding-title">Powered by Blackhat_bullet</div>
|
||||||
|
<div class="footer-branding-links">
|
||||||
|
<a href="https://www.youtube.com/@Blackhat_bullet" target="_blank" class="footer-branding-link">📺 YouTube Channel</a>
|
||||||
|
<a href="https://t.me/+VJJt9csJoEUxZTA1" target="_blank" class="footer-branding-link">💬 Telegram Group</a>
|
||||||
|
<a href="https://t.me/Tempotpsms_bot" target="_blank" class="footer-branding-link">📞 Temp SMS</a>
|
||||||
|
<a href="https://cardbingenerator.com/" target="_blank" class="footer-branding-link">💳 BIN Generator</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Tab -->
|
||||||
|
<div id="settings-tab" class="tab-content">
|
||||||
|
<div class="sub-tabs">
|
||||||
|
<button class="sub-tab-btn active" data-subtab="addresses">
|
||||||
|
<span>🏠 Addresses</span>
|
||||||
|
</button>
|
||||||
|
<button class="sub-tab-btn" data-subtab="names">
|
||||||
|
<span>👤 Names</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Addresses SubTab -->
|
||||||
|
<div id="addresses-subtab" class="sub-tab-content active">
|
||||||
|
<div class="form-group-compact">
|
||||||
|
<input type="text" id="nameInput" placeholder="Full Name" class="form-input">
|
||||||
|
<input type="text" id="address1Input" placeholder="Address Line 1" class="form-input">
|
||||||
|
<input type="text" id="address2Input" placeholder="Address Line 2 (optional)" class="form-input">
|
||||||
|
<div class="input-row">
|
||||||
|
<input type="text" id="cityInput" placeholder="City" class="form-input">
|
||||||
|
<input type="text" id="stateInput" placeholder="State" class="form-input">
|
||||||
|
</div>
|
||||||
|
<input type="text" id="zipInput" placeholder="ZIP Code" class="form-input">
|
||||||
|
<button id="addAddressBtn" class="btn btn-secondary btn-sm">
|
||||||
|
<span class="btn-icon">+</span>
|
||||||
|
<span>Add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-container-compact">
|
||||||
|
<div id="addressesList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Names SubTab -->
|
||||||
|
<div id="names-subtab" class="sub-tab-content">
|
||||||
|
<div class="form-group-compact">
|
||||||
|
<div class="input-row">
|
||||||
|
<input type="text" id="firstNameInput" placeholder="First Name" class="form-input">
|
||||||
|
<input type="text" id="lastNameInput" placeholder="Last Name" class="form-input">
|
||||||
|
</div>
|
||||||
|
<button id="addNameBtn" class="btn btn-secondary btn-sm">
|
||||||
|
<span class="btn-icon">+</span>
|
||||||
|
<span>Add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-container-compact">
|
||||||
|
<div id="namesList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Branding -->
|
||||||
|
<div class="footer-branding">
|
||||||
|
<div class="footer-branding-title">Blackhat_bullet</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
|
||||||
|
Follow us for more tools, hacks, and tutorials
|
||||||
|
</div>
|
||||||
|
<div class="footer-branding-links">
|
||||||
|
<a href="https://www.youtube.com/@Blackhat_bullet" target="_blank" class="footer-branding-link">📺 YouTube: Stuffs</a>
|
||||||
|
<a href="https://t.me/+VJJt9csJoEUxZTA1" target="_blank" class="footer-branding-link">💬 Telegram Group</a>
|
||||||
|
<a href="https://t.me/Tempotpsms_bot" target="_blank" class="footer-branding-link">📞 Temp SMS Bot</a>
|
||||||
|
<a href="https://cardbingenerator.com/" target="_blank" class="footer-branding-link">💳 BIN Generator</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
627
popup.js
Normal file
627
popup.js
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
/**
|
||||||
|
* 界面元素引用 (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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Telegram 广告弹窗逻辑
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const telegramModal = document.getElementById("telegramModal");
|
||||||
|
const modalCloseBtn = document.getElementById("modalCloseBtn");
|
||||||
|
const modalTimer = document.getElementById("modalTimer");
|
||||||
|
let countdownInterval = null;
|
||||||
|
let autoCloseTimeout = null;
|
||||||
|
|
||||||
|
function showTelegramModal() {
|
||||||
|
telegramModal.classList.add("show");
|
||||||
|
let seconds = 5;
|
||||||
|
modalTimer.textContent = seconds;
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
seconds--;
|
||||||
|
if (seconds > 0) {
|
||||||
|
modalTimer.textContent = seconds;
|
||||||
|
} else {
|
||||||
|
modalTimer.textContent = "0";
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 5秒后自动关闭
|
||||||
|
autoCloseTimeout = setTimeout(() => {
|
||||||
|
closeTelegramModal();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTelegramModal() {
|
||||||
|
telegramModal.classList.remove("show");
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
if (autoCloseTimeout) {
|
||||||
|
clearTimeout(autoCloseTimeout);
|
||||||
|
autoCloseTimeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalCloseBtn) {
|
||||||
|
modalCloseBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
closeTelegramModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟显示广告,避免一打开就弹
|
||||||
|
if (telegramModal && modalCloseBtn && modalTimer) {
|
||||||
|
setTimeout(() => {
|
||||||
|
showTelegramModal();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
1486
styles.css
Normal file
1486
styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user