Fix PayPal hosted checkout flow
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-48
@@ -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)),
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -203,7 +203,6 @@
|
||||
commands: [
|
||||
'paypal-approve',
|
||||
'paypal-hosted-email',
|
||||
'paypal-hosted-verification',
|
||||
'paypal-hosted-card',
|
||||
'paypal-hosted-create-account',
|
||||
'paypal-hosted-review',
|
||||
|
||||
@@ -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', `
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user