934 lines
32 KiB
JavaScript
934 lines
32 KiB
JavaScript
// content/paypal-flow.js — PayPal login and approval helper.
|
||
|
||
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
|
||
|
||
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
|
||
const PAYPAL_HOSTED_DEFAULT_PHONE = '1234567890';
|
||
const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
|
||
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
||
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
||
const PAYPAL_HOSTED_STAGE_VERIFICATION = 'verification';
|
||
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
||
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||
const PAYPAL_HOSTED_STEP_KEYS = {
|
||
[PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email',
|
||
[PAYPAL_HOSTED_STAGE_VERIFICATION]: 'paypal-hosted-verification',
|
||
[PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card',
|
||
[PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account',
|
||
[PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review',
|
||
};
|
||
|
||
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
|
||
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
|
||
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
if (
|
||
message.type === 'PAYPAL_GET_STATE'
|
||
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|
||
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|
||
|| message.type === 'PAYPAL_CLICK_APPROVE'
|
||
|| message.type === 'PAYPAL_HOSTED_GET_STATE'
|
||
|| message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
|
||
) {
|
||
resetStopState();
|
||
handlePayPalCommand(message).then((result) => {
|
||
sendResponse({ ok: true, ...(result || {}) });
|
||
}).catch((err) => {
|
||
if (isStopError(err)) {
|
||
sendResponse({ stopped: true, error: err.message });
|
||
return;
|
||
}
|
||
sendResponse({ error: err.message });
|
||
});
|
||
return true;
|
||
}
|
||
});
|
||
} else {
|
||
console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册');
|
||
}
|
||
|
||
async function performPayPalOperationWithDelay(metadata, operation) {
|
||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||
}
|
||
|
||
async function handlePayPalCommand(message) {
|
||
switch (message.type) {
|
||
case 'PAYPAL_GET_STATE':
|
||
return inspectPayPalState();
|
||
case 'PAYPAL_SUBMIT_LOGIN':
|
||
return submitPayPalLogin(message.payload || {});
|
||
case 'PAYPAL_DISMISS_PROMPTS':
|
||
return dismissPayPalPrompts();
|
||
case 'PAYPAL_CLICK_APPROVE':
|
||
return clickPayPalApprove();
|
||
case 'PAYPAL_HOSTED_GET_STATE':
|
||
return inspectPayPalHostedState();
|
||
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
|
||
return runPayPalHostedCheckoutStep(message.payload || {});
|
||
default:
|
||
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
|
||
}
|
||
}
|
||
|
||
async function waitUntil(predicate, options = {}) {
|
||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||
const startedAt = Date.now();
|
||
while (true) {
|
||
throwIfStopped();
|
||
const value = await predicate();
|
||
if (value) {
|
||
return value;
|
||
}
|
||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||
throw new Error(options.timeoutMessage || 'PayPal page timed out waiting for target state.');
|
||
}
|
||
await sleep(intervalMs);
|
||
}
|
||
}
|
||
|
||
async function waitForDocumentComplete() {
|
||
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
|
||
await sleep(1000);
|
||
}
|
||
|
||
function isVisibleElement(el) {
|
||
if (!el) return false;
|
||
let node = el;
|
||
while (node && node.nodeType === 1) {
|
||
if (node.hidden || node.getAttribute?.('aria-hidden') === 'true' || node.getAttribute?.('inert') !== null) {
|
||
return false;
|
||
}
|
||
const nodeStyle = window.getComputedStyle(node);
|
||
if (
|
||
nodeStyle.display === 'none'
|
||
|| nodeStyle.visibility === 'hidden'
|
||
|| nodeStyle.visibility === 'collapse'
|
||
|| Number(nodeStyle.opacity) === 0
|
||
) {
|
||
return false;
|
||
}
|
||
node = node.parentElement;
|
||
}
|
||
const style = window.getComputedStyle(el);
|
||
const rect = el.getBoundingClientRect();
|
||
return style.display !== 'none'
|
||
&& style.visibility !== 'hidden'
|
||
&& Number(rect.width) > 0
|
||
&& Number(rect.height) > 0;
|
||
}
|
||
|
||
function normalizeText(text = '') {
|
||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function getActionText(el) {
|
||
return normalizeText([
|
||
el?.textContent,
|
||
el?.value,
|
||
el?.getAttribute?.('aria-label'),
|
||
el?.getAttribute?.('title'),
|
||
el?.getAttribute?.('placeholder'),
|
||
el?.getAttribute?.('name'),
|
||
el?.id,
|
||
].filter(Boolean).join(' '));
|
||
}
|
||
|
||
function getVisibleControls(selector) {
|
||
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||
}
|
||
|
||
function isEnabledControl(el) {
|
||
return Boolean(el)
|
||
&& !el.disabled
|
||
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||
}
|
||
|
||
function findClickableByText(patterns) {
|
||
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
|
||
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
|
||
return candidates.find((el) => {
|
||
const text = getActionText(el);
|
||
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||
}) || null;
|
||
}
|
||
|
||
function findInputByPatterns(patterns) {
|
||
const inputs = getVisibleControls('input')
|
||
.filter((input) => {
|
||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||
});
|
||
return inputs.find((input) => {
|
||
const text = getActionText(input);
|
||
return patterns.some((pattern) => pattern.test(text));
|
||
}) || null;
|
||
}
|
||
|
||
function findEmailInput() {
|
||
const isPasswordCandidate = (input) => {
|
||
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||
const metadataText = normalizeText([
|
||
input?.textContent,
|
||
input?.getAttribute?.('aria-label'),
|
||
input?.getAttribute?.('title'),
|
||
input?.getAttribute?.('placeholder'),
|
||
input?.getAttribute?.('name'),
|
||
input?.id,
|
||
].filter(Boolean).join(' '));
|
||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||
};
|
||
const inputs = getVisibleControls('input')
|
||
.filter((input) => {
|
||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||
return isEnabledControl(input)
|
||
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type)
|
||
&& !isPasswordCandidate(input);
|
||
});
|
||
return inputs.find((input) => [
|
||
/email|login|user|账号|邮箱/i,
|
||
].some((pattern) => pattern.test(getActionText(input))))
|
||
|| getVisibleControls('input[type="email"]').find((input) => isVisibleElement(input) && !isPasswordCandidate(input))
|
||
|| null;
|
||
}
|
||
|
||
function findPasswordInput() {
|
||
const inputs = getVisibleControls('input')
|
||
.filter((input) => {
|
||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||
});
|
||
return inputs.find((input) => {
|
||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||
const metadataText = normalizeText([
|
||
input?.textContent,
|
||
input?.getAttribute?.('aria-label'),
|
||
input?.getAttribute?.('title'),
|
||
input?.getAttribute?.('placeholder'),
|
||
input?.getAttribute?.('name'),
|
||
input?.id,
|
||
].filter(Boolean).join(' '));
|
||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||
}) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
|
||
}
|
||
|
||
function findLoginNextButton() {
|
||
return findClickableByText([
|
||
/next|continue|login|log\s*in|sign\s*in/i,
|
||
/下一步|继续|登录|登入/i,
|
||
]);
|
||
}
|
||
|
||
function findEmailNextButton() {
|
||
return findClickableByText([
|
||
/next|btn\s*next|btnnext/i,
|
||
/下一页|下一步/i,
|
||
]);
|
||
}
|
||
|
||
function findPasswordLoginButton() {
|
||
const button = findClickableByText([
|
||
/login|log\s*in|sign\s*in/i,
|
||
/登录|登入/i,
|
||
]);
|
||
return button && button !== findEmailNextButton() ? button : null;
|
||
}
|
||
|
||
function findApproveButton() {
|
||
return findClickableByText([
|
||
/同意并继续|同意|继续|授权|确认并继续/i,
|
||
/agree\s*(?:and)?\s*continue|continue|accept|authorize|agree|pay\s*now/i,
|
||
]);
|
||
}
|
||
|
||
function getPayPalPathname() {
|
||
return String(location?.pathname || '').trim();
|
||
}
|
||
|
||
function findHostedVerificationInputs() {
|
||
return Array.from({ length: 6 }, (_, index) => document.getElementById(`ci-ciBasic-${index}`))
|
||
.filter((input) => input && isVisibleElement(input) && isEnabledControl(input));
|
||
}
|
||
|
||
function hasHostedVerificationInputs() {
|
||
return findHostedVerificationInputs().length >= 6;
|
||
}
|
||
|
||
function isHostedLoginPage() {
|
||
return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email'));
|
||
}
|
||
|
||
function isHostedGuestCheckoutPage() {
|
||
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
||
return true;
|
||
}
|
||
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||
if (/create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
|
||
&& findHostedCreateAccountButton()) {
|
||
return false;
|
||
}
|
||
return /\/checkoutweb\//i.test(getPayPalPathname())
|
||
&& Boolean(document.getElementById('phone') || document.getElementById('email'));
|
||
}
|
||
|
||
function isHostedReviewPage() {
|
||
return /\/webapps\/hermes/i.test(getPayPalPathname());
|
||
}
|
||
|
||
function findHostedCreateAccountButton() {
|
||
return document.getElementById('createAccount')
|
||
|| document.getElementById('createAccountButton')
|
||
|| document.querySelector('button[data-testid="createAccountButton"]')
|
||
|| document.querySelector('button[data-testid="create-account-button"]')
|
||
|| findClickableByText([
|
||
/agree\s*(?:&|and)?\s*create\s*(?:paypal\s*)?account/i,
|
||
/create\s*(?:paypal\s*)?account/i,
|
||
/同意.*创建|创建.*账户|创建.*账号/i,
|
||
]);
|
||
}
|
||
|
||
function isHostedCreateAccountPage() {
|
||
if (isHostedLoginPage()) {
|
||
return false;
|
||
}
|
||
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
||
return false;
|
||
}
|
||
const button = findHostedCreateAccountButton();
|
||
if (!button || !isVisibleElement(button) || !isEnabledControl(button)) {
|
||
return false;
|
||
}
|
||
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||
return /create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
|
||
|| /create/i.test(getActionText(button));
|
||
}
|
||
|
||
function findHostedReviewConsentButton() {
|
||
const direct = document.getElementById('consentButton')
|
||
|| document.querySelector('button[data-testid="consentButton"]');
|
||
if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
|
||
return direct;
|
||
}
|
||
return findClickableByText([
|
||
/agree\s*(?:and)?\s*continue|accept|continue/i,
|
||
/同意并继续|同意|继续/i,
|
||
]);
|
||
}
|
||
|
||
function detectPayPalHostedStage() {
|
||
if (!/paypal\./i.test(String(location?.host || ''))) {
|
||
return PAYPAL_HOSTED_STAGE_OUTSIDE;
|
||
}
|
||
if (hasHostedVerificationInputs()) {
|
||
return PAYPAL_HOSTED_STAGE_VERIFICATION;
|
||
}
|
||
if (isHostedGuestCheckoutPage()) {
|
||
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
|
||
}
|
||
if (isHostedReviewPage() && findHostedReviewConsentButton()) {
|
||
return PAYPAL_HOSTED_STAGE_REVIEW;
|
||
}
|
||
if (isHostedCreateAccountPage()) {
|
||
return PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT;
|
||
}
|
||
if (isHostedLoginPage()) {
|
||
return PAYPAL_HOSTED_STAGE_LOGIN;
|
||
}
|
||
return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
|
||
}
|
||
|
||
function fillHostedInputById(id, value) {
|
||
const input = document.getElementById(String(id || '').trim());
|
||
if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
|
||
return false;
|
||
}
|
||
fillInput(input, String(value || ''));
|
||
return true;
|
||
}
|
||
|
||
function selectHostedOptionByIdText(id, value) {
|
||
const select = document.getElementById(String(id || '').trim());
|
||
const expected = normalizeText(value).toLowerCase();
|
||
if (!select || !expected) {
|
||
return false;
|
||
}
|
||
const option = Array.from(select.options || []).find((item) => {
|
||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||
return optionText.includes(expected) || optionValue.includes(expected);
|
||
});
|
||
if (!option) {
|
||
return false;
|
||
}
|
||
select.value = option.value;
|
||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||
return true;
|
||
}
|
||
|
||
function findHostedSubmitButton() {
|
||
return document.querySelector('button[data-testid="submit-button"]')
|
||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||
|| document.querySelector('button.SubmitButton--complete')
|
||
|| findEmailNextButton()
|
||
|| findLoginNextButton()
|
||
|| findClickableByText([
|
||
/pay|continue|next|agree|subscribe/i,
|
||
/支付|继续|下一步|同意|订阅/i,
|
||
]);
|
||
}
|
||
|
||
function getHostedStepKey(stage = '', fallback = 'plus-checkout-create') {
|
||
return PAYPAL_HOSTED_STEP_KEYS[stage] || fallback;
|
||
}
|
||
|
||
async function clickHostedSubmitButton(options = {}) {
|
||
const stepKey = String(options.stepKey || getHostedStepKey(options.stage)).trim();
|
||
const label = String(options.label || 'hosted-paypal-submit').trim();
|
||
const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || 3));
|
||
let lastButtonText = '';
|
||
let lastDisabled = false;
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||
const button = await waitUntil(() => {
|
||
const candidate = findHostedSubmitButton();
|
||
return candidate && isVisibleElement(candidate) ? candidate : null;
|
||
}, {
|
||
intervalMs: 500,
|
||
timeoutMs: 15000,
|
||
timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
|
||
});
|
||
lastButtonText = getActionText(button);
|
||
lastDisabled = !isEnabledControl(button);
|
||
if (lastDisabled) {
|
||
if (attempt >= maxAttempts) {
|
||
throw new Error('PayPal hosted checkout 继续/提交按钮长时间不可用。');
|
||
}
|
||
await sleep(1000);
|
||
continue;
|
||
}
|
||
|
||
await performPayPalOperationWithDelay({ stepKey, kind: 'click', label }, async () => {
|
||
simulateClick(button);
|
||
});
|
||
await sleep(1000);
|
||
return {
|
||
clicked: true,
|
||
buttonText: lastButtonText,
|
||
verificationRequired: hasHostedVerificationInputs(),
|
||
attempt,
|
||
};
|
||
}
|
||
return {
|
||
clicked: false,
|
||
buttonText: lastButtonText,
|
||
disabled: lastDisabled,
|
||
};
|
||
}
|
||
|
||
async function clickHostedEmailNextButton() {
|
||
const button = await waitUntil(() => {
|
||
const candidate = findEmailNextButton();
|
||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||
}, {
|
||
intervalMs: 500,
|
||
timeoutMs: 15000,
|
||
timeoutMessage: 'PayPal hosted checkout 未找到邮箱页“下一页”按钮。',
|
||
});
|
||
const buttonText = getActionText(button);
|
||
await performPayPalOperationWithDelay({
|
||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_LOGIN),
|
||
kind: 'click',
|
||
label: 'hosted-paypal-email-next',
|
||
}, async () => {
|
||
simulateClick(button);
|
||
});
|
||
await sleep(1000);
|
||
return {
|
||
clicked: true,
|
||
buttonText,
|
||
verificationRequired: hasHostedVerificationInputs(),
|
||
};
|
||
}
|
||
|
||
function normalizeHostedPhoneDigits(value = '') {
|
||
return String(value || '').replace(/\D/g, '');
|
||
}
|
||
|
||
function verifyHostedPhoneBeforeSubmit(expectedPhone = '') {
|
||
const phoneInput = document.getElementById('phone');
|
||
if (!phoneInput || !isVisibleElement(phoneInput)) {
|
||
throw new Error('PayPal hosted checkout 未找到电话输入框。');
|
||
}
|
||
const expectedDigits = normalizeHostedPhoneDigits(expectedPhone || PAYPAL_HOSTED_DEFAULT_PHONE);
|
||
const renderedDigits = normalizeHostedPhoneDigits(phoneInput.value || '');
|
||
if (!expectedDigits) {
|
||
throw new Error('PayPal hosted checkout 电话配置为空。');
|
||
}
|
||
const comparableRenderedDigits = renderedDigits.length > expectedDigits.length
|
||
? renderedDigits.slice(-expectedDigits.length)
|
||
: renderedDigits;
|
||
if (comparableRenderedDigits !== expectedDigits) {
|
||
throw new Error(`PayPal hosted checkout 电话不一致:配置 ${expectedDigits},页面 ${renderedDigits || '(空)'}。`);
|
||
}
|
||
return {
|
||
payloadPhoneDigits: expectedDigits,
|
||
renderedPhoneDigits: renderedDigits,
|
||
phoneMatched: true,
|
||
};
|
||
}
|
||
|
||
async function clickHostedCreateAccount(payload = {}) {
|
||
await waitForDocumentComplete();
|
||
const button = await waitUntil(() => {
|
||
const candidate = findHostedCreateAccountButton();
|
||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||
}, {
|
||
intervalMs: 500,
|
||
timeoutMs: 30000,
|
||
timeoutMessage: 'PayPal hosted checkout 未找到创建账号确认按钮。',
|
||
});
|
||
await performPayPalOperationWithDelay({
|
||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT),
|
||
kind: 'click',
|
||
label: 'hosted-paypal-create-account',
|
||
}, async () => {
|
||
simulateClick(button);
|
||
});
|
||
return {
|
||
stage: PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT,
|
||
clicked: true,
|
||
submitted: true,
|
||
buttonText: getActionText(button),
|
||
};
|
||
}
|
||
|
||
function buildHostedRandomEmail() {
|
||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||
let value = '';
|
||
for (let index = 0; index < 16; index += 1) {
|
||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||
}
|
||
return `${value}@gmail.com`;
|
||
}
|
||
|
||
function buildHostedRandomPassword() {
|
||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
|
||
let value = 'Aa1!';
|
||
while (value.length < 14) {
|
||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function buildHostedVisaCard() {
|
||
const digits = [4, 1, 4, 7];
|
||
while (digits.length < 15) {
|
||
digits.push(Math.floor(Math.random() * 10));
|
||
}
|
||
const reversed = digits.slice().reverse();
|
||
let sum = 0;
|
||
for (let index = 0; index < reversed.length; index += 1) {
|
||
let digit = reversed[index];
|
||
if (index % 2 === 0) {
|
||
digit *= 2;
|
||
if (digit > 9) digit -= 9;
|
||
}
|
||
sum += digit;
|
||
}
|
||
digits.push((10 - (sum % 10)) % 10);
|
||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||
const year = (new Date().getFullYear() % 100) + 3;
|
||
return {
|
||
number: digits.join(''),
|
||
expiry: `${month} / ${year}`,
|
||
cvv: String(Math.floor(100 + Math.random() * 900)),
|
||
};
|
||
}
|
||
|
||
async function submitHostedLogin(payload = {}) {
|
||
await waitForDocumentComplete();
|
||
const email = normalizeText(payload.email || buildHostedRandomEmail());
|
||
const emailInput = document.getElementById('email') || findEmailInput();
|
||
if (!emailInput) {
|
||
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
||
}
|
||
refillPayPalEmailInput(emailInput, email);
|
||
const clickResult = await clickHostedEmailNextButton();
|
||
return {
|
||
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
||
submitted: true,
|
||
generatedEmail: email,
|
||
verificationRequired: Boolean(clickResult.verificationRequired),
|
||
};
|
||
}
|
||
|
||
async function fillHostedVerificationCode(payload = {}) {
|
||
await waitForDocumentComplete();
|
||
const code = String(payload.verificationCode || payload.code || '').replace(/\D+/g, '').slice(0, 6);
|
||
if (code.length !== 6) {
|
||
throw new Error('PayPal hosted checkout 验证码无效。');
|
||
}
|
||
const inputs = findHostedVerificationInputs();
|
||
if (inputs.length < 6) {
|
||
throw new Error('PayPal hosted checkout 当前页面未显示验证码输入框。');
|
||
}
|
||
await performPayPalOperationWithDelay({
|
||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_VERIFICATION),
|
||
kind: 'fill',
|
||
label: 'hosted-paypal-verification-code',
|
||
}, async () => {
|
||
inputs.forEach((input, index) => fillInput(input, code[index] || ''));
|
||
});
|
||
return {
|
||
stage: PAYPAL_HOSTED_STAGE_VERIFICATION,
|
||
codeSubmitted: true,
|
||
};
|
||
}
|
||
|
||
async function fillHostedGuestCheckout(payload = {}) {
|
||
await waitForDocumentComplete();
|
||
const countrySelect = document.getElementById('country');
|
||
if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
|
||
countrySelect.value = 'US';
|
||
countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||
await sleep(1000);
|
||
}
|
||
const generatedCard = buildHostedVisaCard();
|
||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||
const values = {
|
||
email: normalizeText(payload.email || buildHostedRandomEmail()),
|
||
phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
|
||
cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
|
||
cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
|
||
cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
|
||
password: String(payload.password || buildHostedRandomPassword()),
|
||
firstName: normalizeText(payload.firstName || 'James'),
|
||
lastName: normalizeText(payload.lastName || 'Smith'),
|
||
};
|
||
fillHostedInputById('email', values.email);
|
||
fillHostedInputById('phone', values.phone);
|
||
fillHostedInputById('cardNumber', values.cardNumber);
|
||
fillHostedInputById('cardExpiry', values.cardExpiry);
|
||
fillHostedInputById('cardCvv', values.cardCvv);
|
||
fillHostedInputById('password', values.password);
|
||
fillHostedInputById('firstName', values.firstName);
|
||
fillHostedInputById('lastName', values.lastName);
|
||
fillHostedInputById('billingLine1', address.street || address.address1 || '');
|
||
fillHostedInputById('billingCity', address.city || '');
|
||
fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
|
||
selectHostedOptionByIdText('billingState', address.state || address.region || '');
|
||
const phoneCheck = verifyHostedPhoneBeforeSubmit(values.phone);
|
||
const clickResult = await clickHostedSubmitButton({
|
||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||
label: 'hosted-paypal-card-submit',
|
||
maxAttempts: 4,
|
||
});
|
||
return {
|
||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||
submitted: true,
|
||
verificationRequired: Boolean(clickResult.verificationRequired),
|
||
payloadPhone: values.phone,
|
||
...phoneCheck,
|
||
};
|
||
}
|
||
|
||
async function clickHostedReviewConsent() {
|
||
await waitForDocumentComplete();
|
||
const button = await waitUntil(() => {
|
||
const candidate = findHostedReviewConsentButton();
|
||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||
}, {
|
||
intervalMs: 500,
|
||
timeoutMs: 30000,
|
||
timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
|
||
});
|
||
await performPayPalOperationWithDelay({
|
||
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_REVIEW),
|
||
kind: 'click',
|
||
label: 'hosted-paypal-review-consent',
|
||
}, async () => {
|
||
simulateClick(button);
|
||
});
|
||
return {
|
||
stage: PAYPAL_HOSTED_STAGE_REVIEW,
|
||
submitted: true,
|
||
};
|
||
}
|
||
|
||
async function runPayPalHostedCheckoutStep(payload = {}) {
|
||
const stage = detectPayPalHostedStage();
|
||
const expectedStage = String(payload.expectedStage || '').trim();
|
||
if (expectedStage && stage !== expectedStage) {
|
||
return {
|
||
stage,
|
||
expectedStage,
|
||
submitted: false,
|
||
skipped: true,
|
||
approveReady: Boolean(findApproveButton()),
|
||
};
|
||
}
|
||
if (stage === PAYPAL_HOSTED_STAGE_VERIFICATION) {
|
||
return payload.verificationCode || payload.code
|
||
? fillHostedVerificationCode(payload)
|
||
: { stage, requiresVerificationCode: true };
|
||
}
|
||
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
|
||
return submitHostedLogin(payload);
|
||
}
|
||
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
|
||
return fillHostedGuestCheckout(payload);
|
||
}
|
||
if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
||
return clickHostedCreateAccount(payload);
|
||
}
|
||
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
||
return clickHostedReviewConsent();
|
||
}
|
||
return {
|
||
stage,
|
||
submitted: false,
|
||
approveReady: Boolean(findApproveButton()),
|
||
};
|
||
}
|
||
|
||
function inspectPayPalHostedState() {
|
||
const stage = detectPayPalHostedStage();
|
||
const createAccountButton = findHostedCreateAccountButton();
|
||
return {
|
||
url: location.href,
|
||
readyState: document.readyState,
|
||
hostedStage: stage,
|
||
verificationInputsVisible: hasHostedVerificationInputs(),
|
||
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
||
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
||
createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)),
|
||
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
||
approveReady: Boolean(findApproveButton()),
|
||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||
};
|
||
}
|
||
|
||
function findPasskeyPromptButtons() {
|
||
const promptPatterns = [
|
||
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
|
||
];
|
||
const bodyText = normalizeText(document.body?.innerText || '');
|
||
const likelyPrompt = promptPatterns.some((pattern) => pattern.test(bodyText));
|
||
if (!likelyPrompt) {
|
||
return [];
|
||
}
|
||
|
||
const cancelOrClose = getVisibleControls('button, a, [role="button"]')
|
||
.filter((el) => {
|
||
const text = getActionText(el);
|
||
return /取消|稍后|不保存|不用|关闭|cancel|not now|maybe later|skip|close|x/i.test(text)
|
||
|| el.getAttribute?.('aria-label')?.match(/close|关闭/i);
|
||
});
|
||
|
||
const iconCloseButtons = getVisibleControls('button, [role="button"]')
|
||
.filter((el) => {
|
||
const text = getActionText(el);
|
||
const rect = el.getBoundingClientRect();
|
||
return (/^×$|^x$/i.test(text) || /close|关闭/i.test(text))
|
||
&& rect.width <= 64
|
||
&& rect.height <= 64;
|
||
});
|
||
|
||
return [...cancelOrClose, ...iconCloseButtons];
|
||
}
|
||
|
||
function hasPasskeyPrompt() {
|
||
return findPasskeyPromptButtons().length > 0;
|
||
}
|
||
|
||
function getPayPalLoginPhase(emailInput, passwordInput) {
|
||
const emailNextButton = findEmailNextButton();
|
||
const passwordLoginButton = findPasswordLoginButton();
|
||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !passwordLoginButton)) {
|
||
return 'email';
|
||
}
|
||
if (emailInput && passwordInput) return 'login_combined';
|
||
if (passwordInput) return 'password';
|
||
if (emailInput) return 'email';
|
||
return '';
|
||
}
|
||
|
||
function refillPayPalEmailInput(emailInput, email) {
|
||
if (!emailInput) return;
|
||
if (typeof emailInput.focus === 'function') {
|
||
emailInput.focus();
|
||
}
|
||
fillInput(emailInput, '');
|
||
fillInput(emailInput, email);
|
||
if (typeof emailInput.blur === 'function') {
|
||
emailInput.blur();
|
||
}
|
||
}
|
||
|
||
async function submitPayPalLogin(payload = {}) {
|
||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||
? performPayPalOperationWithDelay
|
||
: async (metadata, operation) => {
|
||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||
};
|
||
await waitForDocumentComplete();
|
||
|
||
const email = normalizeText(payload.email || '');
|
||
const password = String(payload.password || '');
|
||
if (!password) {
|
||
throw new Error('PayPal 密码为空,请先在侧边栏配置。');
|
||
}
|
||
|
||
let passwordInput = findPasswordInput();
|
||
const emailInput = findEmailInput();
|
||
const emailNextButton = findEmailNextButton();
|
||
|
||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !findPasswordLoginButton())) {
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||
refillPayPalEmailInput(emailInput, email);
|
||
simulateClick(emailNextButton);
|
||
});
|
||
return {
|
||
submitted: false,
|
||
phase: 'email_submitted',
|
||
awaiting: 'password_page',
|
||
};
|
||
}
|
||
|
||
if (!passwordInput && emailInput && email) {
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||
refillPayPalEmailInput(emailInput, email);
|
||
const nextButton = await waitUntil(() => {
|
||
const button = findEmailNextButton() || findLoginNextButton();
|
||
return button && isEnabledControl(button) ? button : null;
|
||
}, {
|
||
intervalMs: 250,
|
||
timeoutMs: 8000,
|
||
timeoutMessage: 'PayPal email page did not expose a clickable next/continue button.',
|
||
});
|
||
simulateClick(nextButton);
|
||
});
|
||
return {
|
||
submitted: false,
|
||
phase: 'email_submitted',
|
||
awaiting: 'password_page',
|
||
};
|
||
} else if (!passwordInput && emailInput && !email) {
|
||
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
|
||
} else if (emailInput && email) {
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'fill', label: 'paypal-email' }, async () => {
|
||
refillPayPalEmailInput(emailInput, email);
|
||
});
|
||
}
|
||
|
||
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), {
|
||
intervalMs: 250,
|
||
timeoutMs: 8000,
|
||
timeoutMessage: 'PayPal password page did not expose a password input.',
|
||
});
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-password' }, async () => {
|
||
fillInput(passwordInput, password);
|
||
await sleep(1000);
|
||
|
||
const loginButton = await waitUntil(() => {
|
||
const button = findClickableByText([
|
||
/login|log\s*in|sign\s*in|continue/i,
|
||
/登录|登入|继续/i,
|
||
]);
|
||
return button && isEnabledControl(button) ? button : null;
|
||
}, {
|
||
intervalMs: 250,
|
||
timeoutMs: 8000,
|
||
timeoutMessage: 'PayPal password page did not expose a clickable login/continue button.',
|
||
});
|
||
|
||
simulateClick(loginButton);
|
||
});
|
||
return {
|
||
submitted: true,
|
||
phase: 'password_submitted',
|
||
awaiting: 'redirect_or_approval',
|
||
};
|
||
}
|
||
|
||
async function dismissPayPalPrompts() {
|
||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||
? performPayPalOperationWithDelay
|
||
: async (metadata, operation) => {
|
||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||
};
|
||
await waitForDocumentComplete();
|
||
const buttons = findPasskeyPromptButtons();
|
||
let clicked = 0;
|
||
for (const button of buttons) {
|
||
if (!isVisibleElement(button) || !isEnabledControl(button)) {
|
||
continue;
|
||
}
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-dismiss-prompt' }, async () => {
|
||
simulateClick(button);
|
||
});
|
||
clicked += 1;
|
||
await sleep(500);
|
||
}
|
||
return {
|
||
clicked,
|
||
hasPromptAfterClick: hasPasskeyPrompt(),
|
||
};
|
||
}
|
||
|
||
async function clickPayPalApprove() {
|
||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||
? performPayPalOperationWithDelay
|
||
: async (metadata, operation) => {
|
||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||
};
|
||
await waitForDocumentComplete();
|
||
await dismissPayPalPrompts().catch(() => ({ clicked: 0 }));
|
||
|
||
const button = findApproveButton();
|
||
if (!button || !isEnabledControl(button)) {
|
||
return {
|
||
clicked: false,
|
||
state: inspectPayPalState(),
|
||
};
|
||
}
|
||
|
||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-approve' }, async () => {
|
||
simulateClick(button);
|
||
});
|
||
return {
|
||
clicked: true,
|
||
buttonText: getActionText(button),
|
||
};
|
||
}
|
||
|
||
function inspectPayPalState() {
|
||
const emailInput = findEmailInput();
|
||
const passwordInput = findPasswordInput();
|
||
const approveButton = findApproveButton();
|
||
const loginPhase = getPayPalLoginPhase(emailInput, passwordInput);
|
||
return {
|
||
url: location.href,
|
||
readyState: document.readyState,
|
||
needsLogin: Boolean(loginPhase),
|
||
loginPhase,
|
||
hasEmailInput: Boolean(emailInput),
|
||
hasPasswordInput: Boolean(passwordInput),
|
||
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
|
||
approveButtonText: approveButton ? getActionText(approveButton) : '',
|
||
hasPasskeyPrompt: hasPasskeyPrompt(),
|
||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||
};
|
||
}
|