// 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 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)); while (true) { throwIfStopped(); const value = await predicate(); if (value) { return value; } await sleep(intervalMs); } } async function waitForDocumentComplete() { await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 }); await sleep(1000); } function isVisibleElement(el) { if (!el) return false; 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 !['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() { return findInputByPatterns([ /email|login|user|账号|邮箱/i, ]) || getVisibleControls('input[type="email"]').find(isVisibleElement) || null; } function findPasswordInput() { return findInputByPatterns([ /password|pass|密码/i, ]) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null; } function findLoginNextButton() { return findClickableByText([ /next|continue|login|log\s*in|sign\s*in/i, /下一步|继续|登录|登入/i, ]); } 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; } async function submitPayPalLogin(payload = {}) { await waitForDocumentComplete(); const email = normalizeText(payload.email || ''); const password = String(payload.password || ''); if (!password) { throw new Error('PayPal 密码为空,请先在侧边栏配置。'); } let passwordInput = findPasswordInput(); const emailInput = findEmailInput(); if (!passwordInput && emailInput && email) { fillInput(emailInput, email); const nextButton = findLoginNextButton(); if (nextButton && isEnabledControl(nextButton)) { simulateClick(nextButton); } passwordInput = await waitUntil(() => findPasswordInput(), { intervalMs: 250 }); } else if (!passwordInput && emailInput && !email) { throw new Error('PayPal 账号为空,请先在侧边栏配置。'); } else if (emailInput && email && !String(emailInput.value || '').trim()) { fillInput(emailInput, email); } passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), { intervalMs: 250 }); 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 }); simulateClick(loginButton); return { submitted: true }; } async function dismissPayPalPrompts() { await waitForDocumentComplete(); const buttons = findPasskeyPromptButtons(); let clicked = 0; for (const button of buttons) { if (!isVisibleElement(button) || !isEnabledControl(button)) { continue; } simulateClick(button); clicked += 1; await sleep(500); } return { clicked, hasPromptAfterClick: hasPasskeyPrompt(), }; } async function clickPayPalApprove() { await waitForDocumentComplete(); await dismissPayPalPrompts().catch(() => ({ clicked: 0 })); const button = findApproveButton(); if (!button || !isEnabledControl(button)) { return { clicked: false, state: inspectPayPalState(), }; } simulateClick(button); return { clicked: true, buttonText: getActionText(button), }; } function inspectPayPalState() { const emailInput = findEmailInput(); const passwordInput = findPasswordInput(); const approveButton = findApproveButton(); return { url: location.href, readyState: document.readyState, needsLogin: Boolean(emailInput || passwordInput), 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), }; }