diff --git a/background.js b/background.js index b5ceb13..b3c9cb4 100644 --- a/background.js +++ b/background.js @@ -10618,7 +10618,6 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'plus-checkout-create', 'paypal-hosted-openai-checkout', 'paypal-hosted-email', - 'paypal-hosted-verification', 'paypal-hosted-card', 'paypal-hosted-create-account', 'paypal-hosted-review', @@ -11723,7 +11722,6 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({ 'plus-checkout-create': 3000, 'paypal-hosted-openai-checkout': 2000, 'paypal-hosted-email': 2000, - 'paypal-hosted-verification': 2000, 'paypal-hosted-card': 2000, 'paypal-hosted-create-account': 2000, 'paypal-hosted-review': 2000, @@ -13744,7 +13742,6 @@ const stepExecutorsByKey = { 'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state), 'paypal-hosted-openai-checkout': (state) => plusCheckoutCreateExecutor.executePayPalHostedOpenAiCheckout(state), 'paypal-hosted-email': (state) => plusCheckoutCreateExecutor.executePayPalHostedEmail(state), - 'paypal-hosted-verification': (state) => plusCheckoutCreateExecutor.executePayPalHostedVerification(state), 'paypal-hosted-card': (state) => plusCheckoutCreateExecutor.executePayPalHostedCard(state), 'paypal-hosted-create-account': (state) => plusCheckoutCreateExecutor.executePayPalHostedCreateAccount(state), 'paypal-hosted-review': (state) => plusCheckoutCreateExecutor.executePayPalHostedReview(state), diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index c2f2991..d29b99f 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -23,24 +23,21 @@ 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_OPENAI_CHECKOUT = 'paypal-hosted-openai-checkout'; const PAYPAL_HOSTED_STEP_EMAIL = 'paypal-hosted-email'; - const PAYPAL_HOSTED_STEP_VERIFICATION = 'paypal-hosted-verification'; const PAYPAL_HOSTED_STEP_CARD = 'paypal-hosted-card'; const PAYPAL_HOSTED_STEP_CREATE_ACCOUNT = 'paypal-hosted-create-account'; const PAYPAL_HOSTED_STEP_REVIEW = 'paypal-hosted-review'; const PAYPAL_HOSTED_STEP_META = Object.freeze({ [PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT]: { step: 6, label: '创建 PayPal 无卡直绑 Checkout' }, [PAYPAL_HOSTED_STEP_EMAIL]: { step: 7, label: '无卡直绑 PayPal 邮箱页' }, - [PAYPAL_HOSTED_STEP_VERIFICATION]: { step: 8, label: '无卡直绑 PayPal 验证码页' }, - [PAYPAL_HOSTED_STEP_CARD]: { step: 9, label: '无卡直绑 PayPal 资料页' }, - [PAYPAL_HOSTED_STEP_CREATE_ACCOUNT]: { step: 10, label: '无卡直绑 PayPal 创建确认页' }, - [PAYPAL_HOSTED_STEP_REVIEW]: { step: 11, label: '无卡直绑 PayPal 授权复核页' }, + [PAYPAL_HOSTED_STEP_CARD]: { step: 8, label: '无卡直绑 PayPal 资料页' }, + [PAYPAL_HOSTED_STEP_CREATE_ACCOUNT]: { step: 9, label: '无卡直绑 PayPal 创建确认页' }, + [PAYPAL_HOSTED_STEP_REVIEW]: { step: 10, label: '无卡直绑 PayPal 授权复核页' }, }); function createPlusCheckoutCreateExecutor(deps = {}) { @@ -454,13 +451,13 @@ async function fetchHostedVerificationCode(verificationUrl = '') { const url = String(verificationUrl || '').trim(); if (!url) { - throw new Error('未配置 PayPal 无卡直绑验证码接口。'); + throw new Error('未配置 OpenAI Checkout 验证码接口。'); } const fetcher = typeof fetchImpl === 'function' ? fetchImpl : (typeof fetch === 'function' ? fetch.bind(globalThis) : null); if (typeof fetcher !== 'function') { - throw new Error('当前运行环境不支持 fetch,无法获取无卡直绑验证码。'); + throw new Error('当前运行环境不支持 fetch,无法获取 OpenAI Checkout 验证码。'); } const separator = url.includes('?') ? '&' : '?'; const response = await fetcher(`${url}${separator}t=${Date.now()}`, { @@ -487,17 +484,17 @@ throwIfStopped(); try { const code = await fetchHostedVerificationCode(verificationUrl); - await addLog(`步骤 6:已获取无卡直绑验证码(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS})。`, 'info'); + await addLog(`步骤 6:已获取 OpenAI Checkout 验证码(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS})。`, 'info'); return code; } catch (error) { lastError = error; - await addLog(`步骤 6:无卡直绑验证码暂不可用(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS}):${error?.message || error}`, 'warn'); + await addLog(`步骤 6:OpenAI Checkout 验证码暂不可用(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS}):${error?.message || error}`, 'warn'); if (attempt < HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS) { await sleepWithStop(HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS); } } } - throw lastError || new Error('无卡直绑验证码轮询失败。'); + throw lastError || new Error('OpenAI Checkout 验证码轮询失败。'); } async function runHostedOpenAiCheckout(tabId, profile, config) { @@ -596,16 +593,14 @@ switch (stage) { case PAYPAL_HOSTED_STAGE_LOGIN: return 1; - case PAYPAL_HOSTED_STAGE_VERIFICATION: - return 2; case PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT: - return 3; + return 2; case PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT: - return 4; + return 3; case PAYPAL_HOSTED_STAGE_REVIEW: - return 5; + return 4; case PAYPAL_HOSTED_STAGE_OUTSIDE: - return 6; + return 5; default: return 0; } @@ -668,6 +663,51 @@ throw new Error(`${label}等待超时。`); } + async function runHostedPayPalStepAndWaitForStageChange(tabId, payload = {}, previousStage = '', options = {}) { + const normalizedPreviousStage = String(previousStage || payload.expectedStage || '').trim(); + const label = String(options.label || 'PayPal 无卡直绑页面跳转').trim(); + const predicate = typeof options.predicate === 'function' + ? options.predicate + : (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== normalizedPreviousStage; + const stageChangePromise = waitForHostedPayPalStage(tabId, predicate, { + label, + timeoutMs: options.timeoutMs || HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS, + intervalMs: options.intervalMs || 500, + }).then( + (nextState) => ({ type: 'stage-change', nextState }), + (error) => ({ type: 'stage-error', error }) + ); + const actionPromise = runHostedPayPalStep(tabId, payload).then( + (result) => ({ type: 'action', result }), + (error) => ({ type: 'action-error', error }) + ); + + const first = await Promise.race([actionPromise, stageChangePromise]); + if (first.type === 'stage-change') { + return { + result: null, + nextState: first.nextState, + completedByStageChange: true, + }; + } + if (first.type === 'action-error') { + throw first.error; + } + if (first.type === 'stage-error') { + throw first.error; + } + + const stageOutcome = await stageChangePromise; + if (stageOutcome.type === 'stage-change') { + return { + result: first.result, + nextState: stageOutcome.nextState, + completedByStageChange: false, + }; + } + throw stageOutcome.error; + } + function resolveCheckoutTargetUrl(result = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) { if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { return String( @@ -799,7 +839,7 @@ } const pageState = await getHostedPayPalState(tabId); - if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_VERIFICATION) + if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) && pageState.hostedStage !== PAYPAL_HOSTED_STAGE_LOGIN) { await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),邮箱节点直接完成。`, 'info'); await completeHostedStep(stepKey, tabId, { @@ -812,61 +852,13 @@ } await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在填写 PayPal 无卡直绑邮箱。`, 'info'); - await runHostedPayPalStep(tabId, { + const { nextState, completedByStageChange } = await runHostedPayPalStepAndWaitForStageChange(tabId, { expectedStage: PAYPAL_HOSTED_STAGE_LOGIN, email: profile.email, - }); - const nextState = await waitForHostedPayPalStage( - tabId, - (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_LOGIN, - { label: `步骤 ${stepNumber}:等待 PayPal 邮箱页跳转` } - ); - await completeHostedStep(stepKey, tabId, { - plusHostedCheckoutLastStage: nextState.hostedStage || '', - }); - } - - async function executePayPalHostedVerification(state = {}) { - const stepKey = PAYPAL_HOSTED_STEP_VERIFICATION; - const stepNumber = getHostedStepNumber(stepKey); - const tabId = await resolveHostedCheckoutTabId(state, stepKey); - if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { - return; + }, PAYPAL_HOSTED_STAGE_LOGIN, { label: `步骤 ${stepNumber}:等待 PayPal 邮箱页跳转` }); + if (completedByStageChange) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:已检测到 PayPal 进入后续页面(${nextState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN}),邮箱节点直接完成。`, 'info'); } - await waitForHostedUrlAfterAction( - tabId, - (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), - { label: `步骤 ${stepNumber}:等待 PayPal 验证码页` } - ); - if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { - return; - } - - const pageState = await getHostedPayPalState(tabId); - if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) - && pageState.hostedStage !== PAYPAL_HOSTED_STAGE_VERIFICATION) { - await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),验证码节点直接完成。`, 'info'); - await completeHostedStep(stepKey, tabId, { - plusHostedCheckoutLastStage: pageState.hostedStage, - }); - return; - } - if (pageState.hostedStage !== PAYPAL_HOSTED_STAGE_VERIFICATION) { - throw new Error(`步骤 ${stepNumber}:当前不是 PayPal 验证码页(当前状态:${pageState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN})。`); - } - - const config = await getHostedCheckoutRuntimeConfig(state); - const verificationCode = await pollHostedVerificationCode(config.verificationUrl); - await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在填写 PayPal 无卡直绑验证码。`, 'info'); - await runHostedPayPalStep(tabId, { - expectedStage: PAYPAL_HOSTED_STAGE_VERIFICATION, - verificationCode, - }); - const nextState = await waitForHostedPayPalStage( - tabId, - (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_VERIFICATION, - { label: `步骤 ${stepNumber}:等待 PayPal 验证码页跳转` } - ); await completeHostedStep(stepKey, tabId, { plusHostedCheckoutLastStage: nextState.hostedStage || '', }); @@ -1459,7 +1451,6 @@ executePlusCheckoutCreate, executePayPalHostedOpenAiCheckout, executePayPalHostedEmail, - executePayPalHostedVerification, executePayPalHostedCard, executePayPalHostedCreateAccount, executePayPalHostedReview, diff --git a/content/paypal-flow.js b/content/paypal-flow.js index 82f9db8..959245d 100644 --- a/content/paypal-flow.js +++ b/content/paypal-flow.js @@ -7,14 +7,12 @@ 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', @@ -249,15 +247,6 @@ 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')); } @@ -323,9 +312,6 @@ 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; } @@ -419,7 +405,6 @@ async function clickHostedSubmitButton(options = {}) { return { clicked: true, buttonText: lastButtonText, - verificationRequired: hasHostedVerificationInputs(), attempt, }; } @@ -447,11 +432,9 @@ async function clickHostedEmailNextButton() { }, async () => { simulateClick(button); }); - await sleep(1000); return { clicked: true, buttonText, - verificationRequired: hasHostedVerificationInputs(), }; } @@ -563,30 +546,7 @@ async function submitHostedLogin(payload = {}) { 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, + clicked: Boolean(clickResult.clicked), }; } @@ -631,7 +591,6 @@ async function fillHostedGuestCheckout(payload = {}) { return { stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT, submitted: true, - verificationRequired: Boolean(clickResult.verificationRequired), payloadPhone: values.phone, ...phoneCheck, }; @@ -672,11 +631,6 @@ async function runPayPalHostedCheckoutStep(payload = {}) { 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); } @@ -703,7 +657,6 @@ function inspectPayPalHostedState() { 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)), diff --git a/content/plus-checkout.js b/content/plus-checkout.js index a502a4f..d29360d 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -252,11 +252,22 @@ async function clickHostedSubmitButton() { }); document.activeElement?.blur?.(); await sleep(300); + const buttonTextBeforeClick = getActionText(button) || '订阅'; + log(`Plus Checkout:准备点击“${buttonTextBeforeClick}”提交 OpenAI Checkout。`); simulateClick(button); - await sleep(1200); + await sleep(300); + const buttonTextAfterClick = getActionText(button); + if (buttonTextAfterClick && SUBSCRIBE_PROCESSING_TEXT_PATTERN.test(buttonTextAfterClick)) { + log(`Plus Checkout:已点击“${buttonTextBeforeClick}”,按钮进入“${buttonTextAfterClick}”,正在等待 PayPal 跳转。`); + } else { + log(`Plus Checkout:已点击“${buttonTextBeforeClick}”,正在等待 PayPal 跳转。`); + } + await sleep(900); return { clicked: true, buttonText: getActionText(button), + buttonTextBeforeClick, + buttonTextAfterClick, hostedVerificationVisible: hasHostedOpenAiVerificationDialog(), }; } diff --git a/content/utils.js b/content/utils.js index f650814..2c439d1 100644 --- a/content/utils.js +++ b/content/utils.js @@ -505,6 +505,7 @@ function simulateClick(el) { : { method: 'click' }; let method = strategy.method || 'click'; + const textBeforeClick = el.textContent || ''; if (method === 'requestSubmit' && form && typeof form.requestSubmit === 'function') { form.requestSubmit(el); @@ -516,8 +517,8 @@ function simulateClick(el) { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } - console.log(LOG_PREFIX, `已点击(${method}): ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`); - log(`已点击(${method}) [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`); + console.log(LOG_PREFIX, `已点击(${method}): ${el.tagName} ${textBeforeClick.slice(0, 30)}`); + log(`已点击(${method}) [${el.tagName}] "${textBeforeClick.trim().slice(0, 30) || ''}"`); } /** diff --git a/data/step-definitions.js b/data/step-definitions.js index 8a06a77..b6ea992 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -36,10 +36,9 @@ const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = [ ...PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6), { id: 7, order: 70, key: 'paypal-hosted-email', title: '无卡直绑填写 PayPal 邮箱', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-email' }, - { id: 8, order: 80, key: 'paypal-hosted-verification', title: '无卡直绑填写 PayPal 验证码', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-verification' }, - { id: 9, order: 90, key: 'paypal-hosted-card', title: '无卡直绑填写 PayPal 资料', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-card' }, - { id: 10, order: 100, key: 'paypal-hosted-create-account', title: '无卡直绑确认创建 PayPal', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-create-account' }, - { id: 11, order: 110, key: 'paypal-hosted-review', title: '无卡直绑完成 PayPal 授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-review' }, + { id: 8, order: 80, key: 'paypal-hosted-card', title: '无卡直绑填写 PayPal 资料', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-card' }, + { id: 9, order: 90, key: 'paypal-hosted-create-account', title: '无卡直绑确认创建 PayPal', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-create-account' }, + { id: 10, order: 100, key: 'paypal-hosted-review', title: '无卡直绑完成 PayPal 授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-review' }, ]; const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [ @@ -239,23 +238,23 @@ ); const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); - const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 12, 120, SIGNUP_METHOD_EMAIL); + const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_EMAIL); const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps( PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, - 12, - 120, + 11, + 110, SIGNUP_METHOD_EMAIL, { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION } ); const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps( PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, - 12, - 120, + 11, + 110, SIGNUP_METHOD_EMAIL, { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION } ); - const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 12, 120, SIGNUP_METHOD_PHONE); - const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 12, 120, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); + const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_PHONE); + const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps( PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, diff --git a/shared/flow-registry.js b/shared/flow-registry.js index 8f31c82..3f3123d 100644 --- a/shared/flow-registry.js +++ b/shared/flow-registry.js @@ -203,7 +203,6 @@ commands: [ 'paypal-approve', 'paypal-hosted-email', - 'paypal-hosted-verification', 'paypal-hosted-card', 'paypal-hosted-create-account', 'paypal-hosted-review', diff --git a/tests/content-utils.test.js b/tests/content-utils.test.js index 44353c8..d7eb289 100644 --- a/tests/content-utils.test.js +++ b/tests/content-utils.test.js @@ -127,6 +127,35 @@ return { shouldReportReadyForFrame }; assert.equal(api.shouldReportReadyForFrame('unknown-source', true), false); }); +test('simulateClick logs the button text captured before click side effects', () => { + const bundle = [extractFunction('simulateClick')].join('\n'); + const logs = []; + const consoleMessages = []; + const api = new Function('logs', 'console', 'location', ` +function throwIfStopped() {} +const LOG_PREFIX = '[test]'; +function log(message) { logs.push(message); } +${bundle} +return { simulateClick }; +`)(logs, { log: (...args) => consoleMessages.push(args.join(' ')) }, { pathname: '/checkout' }); + + const button = { + tagName: 'BUTTON', + textContent: '订阅', + getAttribute: () => '', + click() { + this.textContent = '正在处理'; + }, + }; + + api.simulateClick(button); + + assert.equal(button.textContent, '正在处理'); + assert.equal(logs.at(-1), '已点击(click) [BUTTON] "订阅"'); + assert.match(consoleMessages.at(-1), /BUTTON 订阅/); + assert.doesNotMatch(logs.at(-1), /正在处理/); +}); + test('getRuntimeScriptSource follows injected source overrides after utils is already loaded', () => { const bundle = [extractFunction('getRuntimeScriptSource')].join('\n'); const api = new Function('window', 'SCRIPT_SOURCE', ` diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index ea7460c..8f8b846 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -494,6 +494,66 @@ test('PayPal no-card binding OpenAI checkout node submits hosted page and comple }); }); +test('PayPal hosted email node completes when Next navigation drops the content response', async () => { + const events = []; + let currentUrl = 'https://www.paypal.com/checkoutweb/pay?token=EC-test'; + const executor = api.createPlusCheckoutCreateExecutor({ + addLog: async (message, level = 'info', options = {}) => events.push({ type: 'log', message, level, options }), + chrome: { + tabs: { + get: async (tabId) => ({ id: tabId, url: currentUrl, status: 'complete' }), + }, + }, + completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }), + ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }), + getState: async () => ({ + hostedCheckoutPhoneNumber: '(415) 555-1234', + }), + registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }), + sendTabMessageUntilStopped: async (tabId, source, message) => { + events.push({ type: 'tab-message', tabId, source, message }); + if (message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP') { + currentUrl = 'https://www.paypal.com/checkoutweb/signup?ba_token=BA-test&token=EC-test'; + return new Promise(() => {}); + } + if (message.type === 'PAYPAL_HOSTED_GET_STATE') { + return { + hostedStage: currentUrl.includes('/signup') + ? 'guest_checkout' + : 'pay_login', + }; + } + throw new Error(`unexpected message type ${message.type}`); + }, + setState: async (payload) => events.push({ type: 'set-state', payload }), + sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }), + waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }), + }); + + await executor.executePayPalHostedEmail({ + plusCheckoutTabId: 85661333, + plusHostedCheckoutGuestProfile: { + email: 'guest@example.com', + phone: '4155551234', + address: { street: '1 Main St', city: 'New York', state: 'New York', zip: '10001' }, + }, + }); + + assert.equal(currentUrl.includes('/signup'), true); + assert.equal( + events.some((event) => event.type === 'complete' && event.step === 'paypal-hosted-email'), + true + ); + assert.equal( + events.some((event) => event.type === 'log' && /已检测到 PayPal 进入后续页面(guest_checkout)/.test(event.message)), + true + ); + assert.equal( + events.some((event) => event.type === 'tab-message' && event.message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'), + true + ); +}); + test('Plus checkout content routes billing operations through the operation delay gate', async () => { const { checkoutEvents, send } = createCheckoutContentHarness(); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index 2118121..e72708e 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -222,7 +222,6 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'fill-profile', 'plus-checkout-create', 'paypal-hosted-email', - 'paypal-hosted-verification', 'paypal-hosted-card', 'paypal-hosted-create-account', 'paypal-hosted-review', @@ -236,9 +235,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(hostedSteps.some((step) => step.key === 'paypal-approve'), false); assert.equal(hostedSteps.some((step) => step.key === 'plus-checkout-return'), false); assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-openai-checkout'), false); + assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-verification'), false); assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-card')?.title, '无卡直绑填写 PayPal 资料'); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 15); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 14); assert.deepStrictEqual( goPaySteps.map((step) => step.key), @@ -316,7 +316,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod plusAccountAccessStrategy: 'sub2api_codex_session', }, previousNodeId: 'paypal-hosted-review', - expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], }, { label: 'gopay', @@ -407,7 +407,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node', plusAccountAccessStrategy: 'cpa_codex_session', }, previousNodeId: 'paypal-hosted-review', - expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], }, { label: 'gopay',