Fix PayPal hosted checkout flow

This commit is contained in:
QLHazyCoder
2026-05-20 12:11:19 +08:00
parent 0b58e1116e
commit d1061b1c4a
10 changed files with 182 additions and 142 deletions
-3
View File
@@ -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),
+62 -71
View File
@@ -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(`步骤 6OpenAI 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,
+1 -48
View File
@@ -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)),
+12 -1
View File
@@ -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(),
};
}
+3 -2
View File
@@ -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) || ''}"`);
}
/**
+10 -11
View File
@@ -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,
-1
View File
@@ -203,7 +203,6 @@
commands: [
'paypal-approve',
'paypal-hosted-email',
'paypal-hosted-verification',
'paypal-hosted-card',
'paypal-hosted-create-account',
'paypal-hosted-review',
+29
View File
@@ -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', `
+60
View File
@@ -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();
+5 -5
View File
@@ -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',