Files
GuJumpgate/content/paypal-flow.js
T

958 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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_REVIEW = 'review_consent';
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
const PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL = '__MULTIPAGE_PAYPAL_HOSTED_HERMES_AUTORUN__';
const PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL = '__MULTIPAGE_PAYPAL_HOSTED_GUEST_SUBMIT__';
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 inspectPayPalState();
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
return runHostedCheckoutStep(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 getPayPalHostedPathname() {
return String(location?.pathname || '').trim();
}
function isPayPalHostedLoginPage() {
const pathname = getPayPalHostedPathname();
return pathname === '/pay'
|| Boolean(document.getElementById('email'));
}
function isPayPalHostedGuestCheckoutPage() {
const pathname = getPayPalHostedPathname();
return /\/checkoutweb\//i.test(pathname)
|| Boolean(document.getElementById('cardNumber'))
|| Boolean(document.getElementById('billingLine1'));
}
function isPayPalHostedReviewPage() {
return /\/webapps\/hermes/i.test(getPayPalHostedPathname());
}
function findHostedVerificationInputs() {
return Array.from({ length: 6 }, (_, index) => document.getElementById(`ci-ciBasic-${index}`))
.filter((input) => isVisibleElement(input));
}
function hasHostedVerificationInputs() {
return findHostedVerificationInputs().length >= 6;
}
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 detectPayPalHostedCheckoutStage() {
if (!/paypal\./i.test(String(location?.host || ''))) {
return PAYPAL_HOSTED_STAGE_OUTSIDE;
}
if (hasHostedVerificationInputs()) {
return PAYPAL_HOSTED_STAGE_VERIFICATION;
}
if (isPayPalHostedGuestCheckoutPage()) {
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
}
if (isPayPalHostedReviewPage() && findHostedReviewConsentButton()) {
return PAYPAL_HOSTED_STAGE_REVIEW;
}
if (isPayPalHostedLoginPage()) {
return PAYPAL_HOSTED_STAGE_LOGIN;
}
if (Boolean(findApproveButton())) {
return PAYPAL_HOSTED_STAGE_APPROVAL;
}
return 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, text) {
const select = document.getElementById(String(id || '').trim());
const expectedText = normalizeText(text);
if (!select || !expectedText || !Array.isArray(Array.from(select.options || []))) {
return false;
}
const match = Array.from(select.options || []).find((option) => {
const label = normalizeText(option?.textContent || option?.label || '');
const value = normalizeText(option?.value || '');
return label.toLowerCase().includes(expectedText.toLowerCase())
|| value.toLowerCase().includes(expectedText.toLowerCase());
});
if (!match) {
return false;
}
select.value = match.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
function removeHostedCaptchaArtifacts() {
let removed = false;
const selectors = [
'#captcha-standalone',
'.captcha-overlay',
'.captcha-container',
];
selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((node) => {
try {
node.remove();
removed = true;
} catch {
// Ignore non-removable overlays.
}
});
});
return removed;
}
function startHostedCaptchaCleanupObserver(timeoutMs = 15000) {
const observer = new MutationObserver(() => {
removeHostedCaptchaArtifacts();
});
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
});
setTimeout(() => observer.disconnect(), Math.max(1000, Number(timeoutMs) || 15000));
return observer;
}
function findHostedGuestSubmitButton() {
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')
|| findClickableByText([
/pay|continue|next|agree|subscribe/i,
/支付|继续|下一步|同意|订阅/i,
]);
}
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 lowercase = 'abcdefghijklmnopqrstuvwxyz';
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const digits = '0123456789';
const symbols = '!@#$%^';
const alphabet = `${lowercase}${uppercase}${digits}${symbols}`;
const value = [
lowercase[Math.floor(Math.random() * lowercase.length)],
uppercase[Math.floor(Math.random() * uppercase.length)],
digits[Math.floor(Math.random() * digits.length)],
symbols[Math.floor(Math.random() * symbols.length)],
];
while (value.length < 14) {
value.push(alphabet[Math.floor(Math.random() * alphabet.length)]);
}
return value.sort(() => Math.random() - 0.5).join('');
}
function buildHostedVisaCard() {
const prefixes = [
[4, 1, 4, 7],
[4, 1, 0, 0],
];
const digits = prefixes[Math.floor(Math.random() * prefixes.length)].slice();
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;
}
const checkDigit = (10 - (sum % 10)) % 10;
digits.push(checkDigit);
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
const currentYear = new Date().getFullYear() % 100;
const year = currentYear + Math.floor(Math.random() * 4) + 2;
const cvv = String(Math.floor(100 + Math.random() * 900));
return {
number: digits.join(''),
expiry: `${month} / ${year}`,
cvv,
};
}
function dispatchHostedGenericClick(button) {
const rect = button.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
};
button.dispatchEvent(new PointerEvent('pointerdown', eventInit));
button.dispatchEvent(new MouseEvent('mousedown', eventInit));
button.dispatchEvent(new PointerEvent('pointerup', eventInit));
button.dispatchEvent(new MouseEvent('mouseup', eventInit));
button.dispatchEvent(new MouseEvent('click', eventInit));
}
async function clickHostedGenericSubmitButton(retries = 0) {
removeHostedCaptchaArtifacts();
const button = findHostedGuestSubmitButton() || findEmailNextButton() || findLoginNextButton();
if (!button) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 未找到可点击的继续/提交按钮。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
const buttonText = normalizeText(button.textContent || '');
if (button.disabled) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 按钮长时间处于 disabled 状态。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
const rect = button.getBoundingClientRect();
if (rect.height === 0) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 按钮长时间不可见。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
dispatchHostedGenericClick(button);
await sleep(1000);
removeHostedCaptchaArtifacts();
if (hasHostedVerificationInputs()) {
return {
clicked: true,
verificationRequired: true,
buttonText,
};
}
const currentText = normalizeText(button.textContent || '');
if (!/processing/i.test(currentText) && currentText === buttonText) {
if (retries >= 10) {
return {
clicked: true,
verificationRequired: false,
buttonText,
retried: true,
};
}
await sleep(2000);
return clickHostedGenericSubmitButton(retries + 1);
}
return {
clicked: true,
verificationRequired: false,
buttonText,
};
}
function normalizeHostedVerificationCode(value = '') {
const digits = String(value || '').replace(/\D+/g, '');
return digits.slice(0, 6);
}
async function submitHostedPayLogin(payload = {}) {
await waitForDocumentComplete();
removeHostedCaptchaArtifacts();
const email = normalizeText(payload.email || buildHostedRandomEmail());
if (!email) {
throw new Error('PayPal hosted checkout 缺少邮箱。');
}
const emailInput = document.getElementById('email') || findEmailInput();
if (!emailInput) {
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
}
await sleep(2000);
refillPayPalEmailInput(emailInput, email);
await sleep(1000);
const clickResult = await clickHostedGenericSubmitButton(0);
return {
stage: PAYPAL_HOSTED_STAGE_LOGIN,
submitted: true,
generatedEmail: email,
verificationRequired: Boolean(clickResult?.verificationRequired),
nextExpected: 'guest_checkout_or_verification',
};
}
async function fillHostedVerificationCode(payload = {}) {
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
? performPayPalOperationWithDelay
: async (_metadata, operation) => operation();
await waitForDocumentComplete();
const code = normalizeHostedVerificationCode(payload.verificationCode || payload.code || '');
if (code.length !== 6) {
throw new Error('PayPal hosted checkout 验证码无效。');
}
const inputs = findHostedVerificationInputs();
if (inputs.length < 6) {
throw new Error('PayPal hosted checkout 当前页面未显示验证码输入框。');
}
await delayOperation({ stepKey: 'plus-checkout-create', 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();
startHostedCaptchaCleanupObserver();
removeHostedCaptchaArtifacts();
log(`PayPal guest checkout:收到 payload.phone=${String(payload?.phone || '').trim() || '(空)'}payload.address=${JSON.stringify(payload?.address || {})}`, 'info');
await sleep(2000);
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(3000);
}
const card = buildHostedVisaCard();
const email = normalizeText(payload.email || buildHostedRandomEmail());
const phone = normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE);
const password = String(payload.password || buildHostedRandomPassword());
const firstName = normalizeText(payload.firstName || 'James');
const lastName = normalizeText(payload.lastName || 'Smith');
const cardNumber = String(payload.cardNumber || card.number).replace(/\s+/g, '');
const cardExpiry = normalizeText(payload.cardExpiry || card.expiry);
const cardCvv = normalizeText(payload.cardCvv || card.cvv);
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
if (!email || !password || !cardNumber || !cardExpiry || !cardCvv) {
throw new Error('PayPal hosted checkout 缺少卡支付所需资料。');
}
fillHostedInputById('email', email);
fillHostedInputById('phone', phone);
fillHostedInputById('cardNumber', cardNumber);
fillHostedInputById('cardExpiry', cardExpiry);
fillHostedInputById('cardCvv', cardCvv);
fillHostedInputById('password', password);
fillHostedInputById('firstName', firstName);
fillHostedInputById('lastName', lastName);
fillHostedInputById('billingLine1', address.street || '');
fillHostedInputById('billingCity', address.city || '');
fillHostedInputById('billingPostalCode', address.zip || '');
fillHostedInputById('billingLine1', address.street || '');
selectHostedOptionByIdText('billingState', address.state || '');
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (!rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL]) {
rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL] = true;
setTimeout(() => {
clickHostedGenericSubmitButton(0).catch((error) => {
log(`PayPal hosted checkout guest submit 失败:${error?.message || error}`, 'warn');
}).finally(() => {
rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL] = false;
});
}, 500);
}
return {
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
submitted: true,
verificationRequired: Boolean(hasHostedVerificationInputs()),
submitScheduled: true,
};
}
async function clickHostedReviewConsent() {
await waitForDocumentComplete();
log(`PayPal Hermes:开始等待账单确认文案。当前 URL:${location.href}`, 'info');
let waited = 0;
while (waited < 30) {
waited += 1;
const pageText = document.body ? document.body.innerText : '';
if (String(pageText || '').includes('Set up once. Pay faster next time')) {
log(`PayPal Hermes:第 ${waited}/30 秒命中目标文案,开始寻找 consentButton。`, 'info');
let button = document.getElementById('consentButton')
|| document.querySelector('button[data-testid="consentButton"]');
if (button) {
log('PayPal Hermes:已找到 consentButton,准备点击 Agree and Continue。', 'info');
button.click();
return {
stage: PAYPAL_HOSTED_STAGE_REVIEW,
submitted: true,
};
}
log('PayPal Hermes:首次未找到 consentButton2 秒后重试一次。', 'warn');
await sleep(2000);
button = document.getElementById('consentButton');
if (button) {
log('PayPal Hermes:重试后找到 consentButton,准备点击 Agree and Continue。', 'info');
button.click();
return {
stage: PAYPAL_HOSTED_STAGE_REVIEW,
submitted: true,
};
}
log('PayPal Hermes:重试后仍未找到 consentButton。', 'warn');
throw new Error('PayPal hosted checkout 未找到 consentButton。');
}
if (waited === 1 || waited % 5 === 0) {
log(`PayPal Hermes:尚未命中目标文案,继续等待(${waited}/30)。`, 'info');
}
await sleep(1000);
}
log('PayPal Hermes:等待 30 秒后仍未命中目标文案。', 'warn');
throw new Error('PayPal hosted checkout 账单确认页超时,未检测到目标文案。');
}
async function runHostedCheckoutStep(payload = {}) {
if (isPayPalHostedReviewPage()) {
return clickHostedReviewConsent();
}
const stage = detectPayPalHostedCheckoutStage();
if (stage === PAYPAL_HOSTED_STAGE_VERIFICATION) {
if (!payload.verificationCode && !payload.code) {
return {
stage,
requiresVerificationCode: true,
};
}
return fillHostedVerificationCode(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
return submitHostedPayLogin(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
return fillHostedGuestCheckout(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
return clickHostedReviewConsent();
}
return {
stage,
submitted: false,
approveReady: Boolean(findApproveButton()),
};
}
function shouldAutoRunHostedHermesReview() {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (!isPayPalHostedReviewPage()) {
return false;
}
if (rootScope[PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL]) {
return false;
}
rootScope[PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL] = true;
return true;
}
function scheduleHostedHermesAutoRun() {
if (!shouldAutoRunHostedHermesReview()) {
return;
}
log(`PayPal Hermes 页面已命中,按油猴脚本方式自动等待并点击 Agree and Continue。当前 URL${location.href}`, 'info');
setTimeout(() => {
clickHostedReviewConsent().then(() => {
log('PayPal Hermes:已按油猴脚本方式执行 Agree and Continue。', 'ok');
}).catch((error) => {
log(`PayPal Hermes:自动点击 Agree and Continue 失败:${error?.message || error}`, 'warn');
});
}, 0);
}
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);
const hostedStage = detectPayPalHostedCheckoutStage();
return {
url: location.href,
readyState: document.readyState,
hostedStage,
needsLogin: Boolean(loginPhase),
loginPhase,
hasEmailInput: Boolean(emailInput),
hasPasswordInput: Boolean(passwordInput),
hasHostedGuestCheckout: hostedStage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
verificationInputsVisible: hasHostedVerificationInputs(),
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
approveButtonText: approveButton ? getActionText(approveButton) : '',
hasPasskeyPrompt: hasPasskeyPrompt(),
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
};
}
scheduleHostedHermesAutoRun();