// 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'; 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' ) { 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(); 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 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), }; }