Fix PayPal hosted checkout flow
This commit is contained in:
@@ -35,12 +35,12 @@
|
|||||||
const PAYPAL_HOSTED_STEP_CREATE_ACCOUNT = 'paypal-hosted-create-account';
|
const PAYPAL_HOSTED_STEP_CREATE_ACCOUNT = 'paypal-hosted-create-account';
|
||||||
const PAYPAL_HOSTED_STEP_REVIEW = 'paypal-hosted-review';
|
const PAYPAL_HOSTED_STEP_REVIEW = 'paypal-hosted-review';
|
||||||
const PAYPAL_HOSTED_STEP_META = Object.freeze({
|
const PAYPAL_HOSTED_STEP_META = Object.freeze({
|
||||||
[PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT]: { step: 7, label: '无卡直绑 OpenAI Checkout' },
|
[PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT]: { step: 6, label: '创建 PayPal 无卡直绑 Checkout' },
|
||||||
[PAYPAL_HOSTED_STEP_EMAIL]: { step: 8, label: '无卡直绑 PayPal 邮箱页' },
|
[PAYPAL_HOSTED_STEP_EMAIL]: { step: 7, label: '无卡直绑 PayPal 邮箱页' },
|
||||||
[PAYPAL_HOSTED_STEP_VERIFICATION]: { step: 9, label: '无卡直绑 PayPal 验证码页' },
|
[PAYPAL_HOSTED_STEP_VERIFICATION]: { step: 8, label: '无卡直绑 PayPal 验证码页' },
|
||||||
[PAYPAL_HOSTED_STEP_CARD]: { step: 10, label: '无卡直绑 PayPal 资料页' },
|
[PAYPAL_HOSTED_STEP_CARD]: { step: 9, label: '无卡直绑 PayPal 资料页' },
|
||||||
[PAYPAL_HOSTED_STEP_CREATE_ACCOUNT]: { step: 11, label: '无卡直绑 PayPal 创建确认页' },
|
[PAYPAL_HOSTED_STEP_CREATE_ACCOUNT]: { step: 10, label: '无卡直绑 PayPal 创建确认页' },
|
||||||
[PAYPAL_HOSTED_STEP_REVIEW]: { step: 12, label: '无卡直绑 PayPal 授权复核页' },
|
[PAYPAL_HOSTED_STEP_REVIEW]: { step: 11, label: '无卡直绑 PayPal 授权复核页' },
|
||||||
});
|
});
|
||||||
|
|
||||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||||
@@ -686,7 +686,7 @@
|
|||||||
throw new Error('步骤 6:PayPal 无卡直绑未返回可用的订阅链接。');
|
throw new Error('步骤 6:PayPal 无卡直绑未返回可用的订阅链接。');
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('步骤 6:PayPal 无卡直绑链接已创建,正在打开 hosted checkout 页面...', 'ok');
|
await addLog('步骤 6:PayPal 无卡直绑链接已创建,正在打开并提交 OpenAI Checkout 页面...', 'ok');
|
||||||
await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
|
await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
|
||||||
await waitForTabCompleteUntilStopped(tabId);
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
|
||||||
@@ -697,26 +697,37 @@
|
|||||||
500
|
500
|
||||||
);
|
);
|
||||||
const landedUrl = String(landedTab?.url || targetCheckoutUrl || '').trim();
|
const landedUrl = String(landedTab?.url || targetCheckoutUrl || '').trim();
|
||||||
|
let completedUrl = landedUrl;
|
||||||
|
|
||||||
const isAlreadySuccessful = isHostedCheckoutSuccessUrl(landedUrl);
|
if (isHostedOpenAiCheckoutUrl(completedUrl)) {
|
||||||
|
const { profile, config } = await ensureHostedGuestProfile(state);
|
||||||
|
await addLog(`步骤 6:正在提交 OpenAI Checkout,等待跳转到 PayPal 邮箱页(电话使用本地号码 ${profile.phone})。`, 'info');
|
||||||
|
completedUrl = String(await runHostedOpenAiCheckout(tabId, profile, config) || await getHostedCurrentUrl(tabId) || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPayPalUrl(completedUrl)) {
|
||||||
|
await waitForTabCompleteUntilStopped(tabId).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadySuccessful = isHostedCheckoutSuccessUrl(completedUrl);
|
||||||
await setState({
|
await setState({
|
||||||
plusCheckoutTabId: tabId,
|
plusCheckoutTabId: tabId,
|
||||||
plusCheckoutUrl: landedUrl,
|
plusCheckoutUrl: completedUrl,
|
||||||
plusCheckoutCountry: result.country || 'US',
|
plusCheckoutCountry: result.country || 'US',
|
||||||
plusCheckoutCurrency: result.currency || 'USD',
|
plusCheckoutCurrency: result.currency || 'USD',
|
||||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||||
plusReturnUrl: isAlreadySuccessful ? landedUrl : '',
|
plusReturnUrl: isAlreadySuccessful ? completedUrl : '',
|
||||||
plusHostedCheckoutCompleted: isAlreadySuccessful,
|
plusHostedCheckoutCompleted: isAlreadySuccessful,
|
||||||
});
|
});
|
||||||
|
|
||||||
await addLog(`步骤 6:PayPal 无卡直绑页面已就绪(${result.country || 'US'} ${result.currency || 'USD'}),准备进入页面级直绑节点。`, 'info');
|
await addLog(`步骤 6:PayPal 无卡直绑已提交 OpenAI Checkout(${result.country || 'US'} ${result.currency || 'USD'}),准备进入 PayPal 邮箱页。`, 'info');
|
||||||
|
|
||||||
await completeNodeFromBackground('plus-checkout-create', {
|
await completeNodeFromBackground('plus-checkout-create', {
|
||||||
plusCheckoutCountry: result.country || 'US',
|
plusCheckoutCountry: result.country || 'US',
|
||||||
plusCheckoutCurrency: result.currency || 'USD',
|
plusCheckoutCurrency: result.currency || 'USD',
|
||||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||||
plusCheckoutUrl: landedUrl,
|
plusCheckoutUrl: completedUrl,
|
||||||
plusReturnUrl: isAlreadySuccessful ? landedUrl : '',
|
plusReturnUrl: isAlreadySuccessful ? completedUrl : '',
|
||||||
plusHostedCheckoutCompleted: isAlreadySuccessful,
|
plusHostedCheckoutCompleted: isAlreadySuccessful,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
||||||
const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i;
|
const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i;
|
||||||
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
|
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
|
||||||
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3;
|
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 5;
|
||||||
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
|
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 10000;
|
||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
@@ -1883,7 +1883,7 @@
|
|||||||
await addLog(
|
await addLog(
|
||||||
attempt === 1
|
attempt === 1
|
||||||
? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...'
|
? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...'
|
||||||
: `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新提交账单地址...`,
|
: `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新检测订阅按钮...`,
|
||||||
attempt === 1 ? 'info' : 'warn'
|
attempt === 1 ? 'info' : 'warn'
|
||||||
);
|
);
|
||||||
await sleepWithStop(3000);
|
await sleepWithStop(3000);
|
||||||
@@ -1907,17 +1907,27 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 ${paymentConfig.label}(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS})...`, 'info');
|
const subscribeClicked = subscribeResult?.clicked !== false;
|
||||||
|
const subscribeButtonText = String(subscribeResult?.subscribeButtonText || '').trim();
|
||||||
|
const subscribeButtonStatus = String(subscribeResult?.subscribeButtonStatus || '').trim();
|
||||||
|
if (subscribeClicked) {
|
||||||
|
await addLog(`步骤 7:已点击订阅按钮,正在等待跳转到 ${paymentConfig.label}(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS})...`, 'info');
|
||||||
|
} else {
|
||||||
|
const buttonStateLabel = subscribeButtonText || subscribeButtonStatus || 'unknown';
|
||||||
|
await addLog(`步骤 7:订阅按钮当前为「${buttonStateLabel}」,本轮未点击,正在等待页面是否跳转到 ${paymentConfig.label}(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS})...`, 'warn');
|
||||||
|
}
|
||||||
redirectedToPayment = await waitForPaymentRedirectAfterSubmit(tabId, paymentMethod);
|
redirectedToPayment = await waitForPaymentRedirectAfterSubmit(tabId, paymentMethod);
|
||||||
if (redirectedToPayment) {
|
if (redirectedToPayment) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`;
|
lastSubmitError = subscribeClicked
|
||||||
await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn');
|
? `点击订阅后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`
|
||||||
|
: `订阅按钮当前为「${subscribeButtonText || subscribeButtonStatus || 'unknown'}」,${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`;
|
||||||
|
await addLog(`步骤 7:${lastSubmitError},将重新检测订阅按钮。`, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!redirectedToPayment) {
|
if (!redirectedToPayment) {
|
||||||
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`);
|
throw new Error(`步骤 7:多次检测订阅按钮后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await completeNodeFromBackground('plus-checkout-billing', {
|
await completeNodeFromBackground('plus-checkout-billing', {
|
||||||
|
|||||||
+29
-4
@@ -292,6 +292,9 @@ function findHostedCreateAccountButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isHostedCreateAccountPage() {
|
function isHostedCreateAccountPage() {
|
||||||
|
if (isHostedLoginPage()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -427,6 +430,31 @@ async function clickHostedSubmitButton(options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clickHostedEmailNextButton() {
|
||||||
|
const button = await waitUntil(() => {
|
||||||
|
const candidate = findEmailNextButton();
|
||||||
|
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||||
|
}, {
|
||||||
|
intervalMs: 500,
|
||||||
|
timeoutMs: 15000,
|
||||||
|
timeoutMessage: 'PayPal hosted checkout 未找到邮箱页“下一页”按钮。',
|
||||||
|
});
|
||||||
|
const buttonText = getActionText(button);
|
||||||
|
await performPayPalOperationWithDelay({
|
||||||
|
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_LOGIN),
|
||||||
|
kind: 'click',
|
||||||
|
label: 'hosted-paypal-email-next',
|
||||||
|
}, async () => {
|
||||||
|
simulateClick(button);
|
||||||
|
});
|
||||||
|
await sleep(1000);
|
||||||
|
return {
|
||||||
|
clicked: true,
|
||||||
|
buttonText,
|
||||||
|
verificationRequired: hasHostedVerificationInputs(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeHostedPhoneDigits(value = '') {
|
function normalizeHostedPhoneDigits(value = '') {
|
||||||
return String(value || '').replace(/\D/g, '');
|
return String(value || '').replace(/\D/g, '');
|
||||||
}
|
}
|
||||||
@@ -530,10 +558,7 @@ async function submitHostedLogin(payload = {}) {
|
|||||||
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
||||||
}
|
}
|
||||||
refillPayPalEmailInput(emailInput, email);
|
refillPayPalEmailInput(emailInput, email);
|
||||||
const clickResult = await clickHostedSubmitButton({
|
const clickResult = await clickHostedEmailNextButton();
|
||||||
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
|
||||||
label: 'hosted-paypal-email-submit',
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
||||||
submitted: true,
|
submitted: true,
|
||||||
|
|||||||
@@ -1524,6 +1524,42 @@ function isBusySubscribeButton(button) {
|
|||||||
|| /loading|processing|submitting|请稍候|处理中|加载中/i.test(text);
|
|| /loading|processing|submitting|请稍候|处理中|加载中/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUBSCRIBE_READY_TEXT_PATTERN = /\u8ba2\u9605|\u7ee7\u7eed|\u786e\u8ba4|\u652f\u4ed8|subscribe|continue|confirm|pay|\u8d2d\u4e70\s*ChatGPT\s*Plus|start\s*subscription|place\s*order/i;
|
||||||
|
const SUBSCRIBE_PROCESSING_TEXT_PATTERN = /\u6b63\u5728\u5904\u7406|\u5904\u7406\u4e2d|\u8bf7\u7a0d\u5019|\u52a0\u8f7d\u4e2d|loading|processing|submitting/i;
|
||||||
|
|
||||||
|
function getSubscribeButtonState(button) {
|
||||||
|
if (!button) {
|
||||||
|
return {
|
||||||
|
found: false,
|
||||||
|
enabled: false,
|
||||||
|
busy: false,
|
||||||
|
ready: false,
|
||||||
|
status: 'missing',
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const text = normalizeText([
|
||||||
|
button.innerText,
|
||||||
|
button.textContent,
|
||||||
|
button.value,
|
||||||
|
button.getAttribute?.('aria-label'),
|
||||||
|
].filter(Boolean).join(' ')) || getActionText(button);
|
||||||
|
const searchText = getCombinedSearchText(button);
|
||||||
|
const combinedText = normalizeText(`${text} ${searchText}`);
|
||||||
|
const enabled = isEnabledControl(button);
|
||||||
|
const busy = Boolean(isBusySubscribeButton(button) || SUBSCRIBE_PROCESSING_TEXT_PATTERN.test(combinedText));
|
||||||
|
const readyText = SUBSCRIBE_READY_TEXT_PATTERN.test(combinedText);
|
||||||
|
const ready = Boolean(enabled && readyText && !busy);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
enabled,
|
||||||
|
busy,
|
||||||
|
ready,
|
||||||
|
status: busy ? 'processing' : (!enabled ? 'disabled' : (readyText ? 'ready' : 'unknown')),
|
||||||
|
text: text || searchText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getAssociatedForm(button) {
|
function getAssociatedForm(button) {
|
||||||
if (!button) return null;
|
if (!button) return null;
|
||||||
if (button.form) return button.form;
|
if (button.form) return button.form;
|
||||||
@@ -1710,12 +1746,23 @@ async function clickPlusSubscribe(payload = {}) {
|
|||||||
|
|
||||||
const subscribeButton = await waitUntil(() => {
|
const subscribeButton = await waitUntil(() => {
|
||||||
const button = findSubscribeButton();
|
const button = findSubscribeButton();
|
||||||
return button && isEnabledControl(button) && !isBusySubscribeButton(button) ? button : null;
|
return button || null;
|
||||||
}, {
|
}, {
|
||||||
label: '订阅按钮',
|
label: '订阅按钮',
|
||||||
intervalMs: 250,
|
intervalMs: 250,
|
||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
});
|
});
|
||||||
|
const buttonState = getSubscribeButtonState(subscribeButton);
|
||||||
|
if (!buttonState.ready) {
|
||||||
|
log(`订阅按钮当前状态 [${buttonState.status}] "${buttonState.text.slice(0, 40)}",本轮不点击`);
|
||||||
|
return {
|
||||||
|
clicked: false,
|
||||||
|
subscribeButtonBusy: buttonState.busy,
|
||||||
|
subscribeButtonEnabled: buttonState.enabled,
|
||||||
|
subscribeButtonStatus: buttonState.status,
|
||||||
|
subscribeButtonText: buttonState.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
|
await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
|
||||||
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'submit', label: 'click-subscribe' }, async () => {
|
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'submit', label: 'click-subscribe' }, async () => {
|
||||||
@@ -1723,6 +1770,8 @@ async function clickPlusSubscribe(payload = {}) {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
clicked: true,
|
clicked: true,
|
||||||
|
subscribeButtonStatus: 'clicked',
|
||||||
|
subscribeButtonText: buttonState.text,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1739,6 +1788,7 @@ async function readChatGptSessionAccessToken() {
|
|||||||
|
|
||||||
async function inspectPlusCheckoutState(options = {}) {
|
async function inspectPlusCheckoutState(options = {}) {
|
||||||
const structuredAddress = getStructuredAddressFields();
|
const structuredAddress = getStructuredAddressFields();
|
||||||
|
const subscribeButtonState = getSubscribeButtonState(findSubscribeButton());
|
||||||
const state = {
|
const state = {
|
||||||
url: location.href,
|
url: location.href,
|
||||||
readyState: document.readyState,
|
readyState: document.readyState,
|
||||||
@@ -1750,7 +1800,11 @@ async function inspectPlusCheckoutState(options = {}) {
|
|||||||
paymentTextPreview: getPaymentTextPreview(),
|
paymentTextPreview: getPaymentTextPreview(),
|
||||||
cardFieldsVisible: hasCreditCardFields(),
|
cardFieldsVisible: hasCreditCardFields(),
|
||||||
billingFieldsVisible: hasBillingAddressFields(),
|
billingFieldsVisible: hasBillingAddressFields(),
|
||||||
hasSubscribeButton: Boolean(findSubscribeButton()),
|
hasSubscribeButton: subscribeButtonState.found,
|
||||||
|
subscribeButtonBusy: subscribeButtonState.busy,
|
||||||
|
subscribeButtonEnabled: subscribeButtonState.enabled,
|
||||||
|
subscribeButtonStatus: subscribeButtonState.status,
|
||||||
|
subscribeButtonText: subscribeButtonState.text,
|
||||||
hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(),
|
hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(),
|
||||||
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
||||||
hostedPayPalButtonFound: Boolean(findHostedPayPalButton()),
|
hostedPayPalButtonFound: Boolean(findHostedPayPalButton()),
|
||||||
|
|||||||
+12
-13
@@ -35,12 +35,11 @@
|
|||||||
];
|
];
|
||||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = [
|
const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = [
|
||||||
...PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6),
|
...PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6),
|
||||||
{ id: 7, order: 70, key: 'paypal-hosted-openai-checkout', title: '无卡直绑提交 OpenAI Checkout', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'paypal-hosted-openai-checkout' },
|
{ 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-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-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-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-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: 12, order: 120, key: 'paypal-hosted-review', title: '无卡直绑完成 PayPal 授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-review' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
|
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
|
||||||
@@ -240,23 +239,23 @@
|
|||||||
);
|
);
|
||||||
const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE);
|
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_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, 13, 130, SIGNUP_METHOD_EMAIL);
|
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_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
||||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
||||||
13,
|
12,
|
||||||
130,
|
120,
|
||||||
SIGNUP_METHOD_EMAIL,
|
SIGNUP_METHOD_EMAIL,
|
||||||
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
|
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
|
||||||
);
|
);
|
||||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
||||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
||||||
13,
|
12,
|
||||||
130,
|
120,
|
||||||
SIGNUP_METHOD_EMAIL,
|
SIGNUP_METHOD_EMAIL,
|
||||||
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
|
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
|
||||||
);
|
);
|
||||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 13, 130, SIGNUP_METHOD_PHONE);
|
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, 13, 130, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
|
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_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
|
const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
|
||||||
const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
|
const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
|
||||||
PLUS_GOPAY_PREFIX_STEP_DEFINITIONS,
|
PLUS_GOPAY_PREFIX_STEP_DEFINITIONS,
|
||||||
|
|||||||
@@ -408,6 +408,11 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
text: 'Agree & Create Account',
|
text: 'Agree & Create Account',
|
||||||
attrs: { 'data-testid': 'createAccountButton' },
|
attrs: { 'data-testid': 'createAccountButton' },
|
||||||
});
|
});
|
||||||
|
const nextButton = createDomElement({
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
id: 'btnNext',
|
||||||
|
text: '下一页',
|
||||||
|
});
|
||||||
|
|
||||||
function setElements(nextElements) {
|
function setElements(nextElements) {
|
||||||
elements = nextElements;
|
elements = nextElements;
|
||||||
@@ -450,6 +455,15 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
setElements([createAccountButton]);
|
setElements([createAccountButton]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPayEmail() {
|
||||||
|
location.href = 'https://www.paypal.com/pay?token=demo';
|
||||||
|
location.host = 'www.paypal.com';
|
||||||
|
location.pathname = '/pay';
|
||||||
|
body.innerText = '请输入您的电子邮箱地址。 下一页 或 创建账户';
|
||||||
|
body.textContent = body.innerText;
|
||||||
|
setElements([emailInput, nextButton, createAccountButton]);
|
||||||
|
}
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
console: { log() {}, warn() {}, error() {}, info() {} },
|
console: { log() {}, warn() {}, error() {}, info() {} },
|
||||||
location,
|
location,
|
||||||
@@ -541,6 +555,7 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
send,
|
send,
|
||||||
|
showPayEmail,
|
||||||
showCreateAccount,
|
showCreateAccount,
|
||||||
showGuestCheckout,
|
showGuestCheckout,
|
||||||
};
|
};
|
||||||
@@ -610,6 +625,40 @@ test('PayPal hosted guest checkout blocks submit when rendered phone differs fro
|
|||||||
assert.equal(harness.events.some((event) => event.type === 'click' && event.id === 'hostedSubmit'), false);
|
assert.equal(harness.events.some((event) => event.type === 'click' && event.id === 'hostedSubmit'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PayPal hosted /pay email page fills email and clicks Next instead of Create Account', async () => {
|
||||||
|
const harness = createHostedPayPalHarness();
|
||||||
|
harness.showPayEmail();
|
||||||
|
|
||||||
|
const state = await harness.send({
|
||||||
|
type: 'PAYPAL_HOSTED_GET_STATE',
|
||||||
|
source: 'test',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
assert.equal(state.ok, true);
|
||||||
|
assert.equal(state.hostedStage, 'pay_login');
|
||||||
|
assert.equal(state.hasHostedEmailInput, true);
|
||||||
|
|
||||||
|
const result = await harness.send({
|
||||||
|
type: 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP',
|
||||||
|
source: 'test',
|
||||||
|
payload: {
|
||||||
|
expectedStage: 'pay_login',
|
||||||
|
email: 'guest@example.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.stage, 'pay_login');
|
||||||
|
assert.equal(result.submitted, true);
|
||||||
|
assert.equal(harness.events.some((event) => event.type === 'fill' && event.id === 'email' && event.value === 'guest@example.com'), true);
|
||||||
|
assert.equal(harness.events.some((event) => event.type === 'click' && event.id === 'btnNext'), true);
|
||||||
|
assert.equal(harness.events.some((event) => event.type === 'click' && event.id === 'createAccountButton'), false);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(harness.events.filter((event) => event.type === 'operation').map((event) => event.metadata))),
|
||||||
|
[{ stepKey: 'paypal-hosted-email', kind: 'click', label: 'hosted-paypal-email-next' }]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('PayPal hosted create account page is detected and handled as its own step', async () => {
|
test('PayPal hosted create account page is detected and handled as its own step', async () => {
|
||||||
const harness = createHostedPayPalHarness();
|
const harness = createHostedPayPalHarness();
|
||||||
harness.showCreateAccount();
|
harness.showCreateAccount();
|
||||||
|
|||||||
@@ -566,6 +566,49 @@ return { findSubscribeButton };
|
|||||||
assert.equal(api.findSubscribeButton(), submitButton);
|
assert.equal(api.findSubscribeButton(), submitButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getSubscribeButtonState does not treat processing subscribe text as clickable', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('getActionText'),
|
||||||
|
extractFunction('getSearchText'),
|
||||||
|
extractFunction('getFieldText'),
|
||||||
|
extractFunction('getCombinedSearchText'),
|
||||||
|
extractFunction('isEnabledControl'),
|
||||||
|
extractFunction('isBusySubscribeButton'),
|
||||||
|
'const SUBSCRIBE_READY_TEXT_PATTERN = /\\u8ba2\\u9605|\\u7ee7\\u7eed|\\u786e\\u8ba4|\\u652f\\u4ed8|subscribe|continue|confirm|pay|\\u8d2d\\u4e70\\s*ChatGPT\\s*Plus|start\\s*subscription|place\\s*order/i;',
|
||||||
|
'const SUBSCRIBE_PROCESSING_TEXT_PATTERN = /\\u6b63\\u5728\\u5904\\u7406|\\u5904\\u7406\\u4e2d|\\u8bf7\\u7a0d\\u5019|\\u52a0\\u8f7d\\u4e2d|loading|processing|submitting/i;',
|
||||||
|
extractFunction('getSubscribeButtonState'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const processingButton = createElement({
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
text: '订阅 正在处理',
|
||||||
|
attrs: { type: 'submit' },
|
||||||
|
});
|
||||||
|
const readyButton = createElement({
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
text: '订阅',
|
||||||
|
attrs: { type: 'submit' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
${bundle}
|
||||||
|
return { getSubscribeButtonState };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
{
|
||||||
|
ready: api.getSubscribeButtonState(processingButton).ready,
|
||||||
|
status: api.getSubscribeButtonState(processingButton).status,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ready: false,
|
||||||
|
status: 'processing',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(api.getSubscribeButtonState(readyButton).ready, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('humanLikeClick submits a detached submit button through its form attribute', async () => {
|
test('humanLikeClick submits a detached submit button through its form attribute', async () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
'function throwIfStopped() {}',
|
'function throwIfStopped() {}',
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ function createExecutorHarness({
|
|||||||
getState = null,
|
getState = null,
|
||||||
queryTabsInAutomationWindow = null,
|
queryTabsInAutomationWindow = null,
|
||||||
markCurrentRegistrationAccountUsed = async () => {},
|
markCurrentRegistrationAccountUsed = async () => {},
|
||||||
|
onClickSubscribe = null,
|
||||||
probeIpProxyExit = null,
|
probeIpProxyExit = null,
|
||||||
onSetState = null,
|
onSetState = null,
|
||||||
sleepWithStop = null,
|
sleepWithStop = null,
|
||||||
@@ -137,8 +138,15 @@ function createExecutorHarness({
|
|||||||
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
||||||
}
|
}
|
||||||
if (message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE') {
|
if (message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE') {
|
||||||
|
if (typeof onClickSubscribe === 'function') {
|
||||||
|
const clickResult = await onClickSubscribe({ checkoutTab, events, frameId, message, tabId });
|
||||||
|
if (clickResult !== undefined) {
|
||||||
|
return clickResult;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
checkoutTab.url = submitRedirectUrl;
|
checkoutTab.url = submitRedirectUrl;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return createSuccessfulBillingResult();
|
return createSuccessfulBillingResult();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -242,6 +250,57 @@ test('Plus checkout billing uses the current checkout tab when step 6 did not re
|
|||||||
assert.equal(events.logs.some((entry) => /当前已在 Plus Checkout 页面/.test(entry.message)), true);
|
assert.equal(events.logs.some((entry) => /当前已在 Plus Checkout 页面/.test(entry.message)), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Plus checkout billing waits on processing subscribe text before clicking a ready subscribe button again', async () => {
|
||||||
|
const originalNow = Date.now;
|
||||||
|
let now = 0;
|
||||||
|
let clickCalls = 0;
|
||||||
|
Date.now = () => now;
|
||||||
|
try {
|
||||||
|
const { events, executor } = createExecutorHarness({
|
||||||
|
frames: [{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' }],
|
||||||
|
stateByFrame: {
|
||||||
|
0: {
|
||||||
|
hasPayPal: true,
|
||||||
|
paypalCandidates: [{ tag: 'button', text: 'PayPal' }],
|
||||||
|
billingFieldsVisible: true,
|
||||||
|
hasSubscribeButton: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onClickSubscribe: async ({ checkoutTab }) => {
|
||||||
|
clickCalls += 1;
|
||||||
|
if (clickCalls === 1) {
|
||||||
|
return {
|
||||||
|
clicked: false,
|
||||||
|
subscribeButtonStatus: 'processing',
|
||||||
|
subscribeButtonText: '订阅正在处理',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
checkoutTab.url = 'https://www.paypal.com/checkoutnow';
|
||||||
|
return {
|
||||||
|
clicked: true,
|
||||||
|
subscribeButtonStatus: 'clicked',
|
||||||
|
subscribeButtonText: '订阅',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sleepWithStop: async (ms) => {
|
||||||
|
events.sleeps.push(ms);
|
||||||
|
now += ms;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePlusCheckoutBilling({});
|
||||||
|
|
||||||
|
const subscribeMessages = events.messages.filter((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE');
|
||||||
|
assert.equal(subscribeMessages.length, 2);
|
||||||
|
assert.equal(subscribeMessages.some((entry) => entry.message.payload.allowBusySubscribeButton !== undefined), false);
|
||||||
|
assert.equal(events.sleeps.filter((ms) => ms === 500).length >= 20, true);
|
||||||
|
assert.equal(events.logs.some((entry) => /本轮未点击/.test(entry.message)), true);
|
||||||
|
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||||
|
} finally {
|
||||||
|
Date.now = originalNow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('Plus checkout billing searches checkout tabs inside the locked automation window', async () => {
|
test('Plus checkout billing searches checkout tabs inside the locked automation window', async () => {
|
||||||
const queries = [];
|
const queries = [];
|
||||||
const { checkoutTab, executor } = createExecutorHarness({
|
const { checkoutTab, executor } = createExecutorHarness({
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
|||||||
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PayPal no-card binding create only opens hosted checkout and leaves page steps to later nodes', async () => {
|
test('PayPal no-card binding create opens and submits hosted OpenAI checkout before completing', async () => {
|
||||||
const events = [];
|
const events = [];
|
||||||
let currentUrl = 'https://chatgpt.com/';
|
let currentUrl = 'https://chatgpt.com/';
|
||||||
const executor = api.createPlusCheckoutCreateExecutor({
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
@@ -315,8 +315,27 @@ test('PayPal no-card binding create only opens hosted checkout and leaves page s
|
|||||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => {
|
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => {
|
||||||
events.push({ type: 'ready', source, tabId, options });
|
events.push({ type: 'ready', source, tabId, options });
|
||||||
},
|
},
|
||||||
fetch: async () => {
|
fetch: async (url) => {
|
||||||
throw new Error('create node should not fetch address or run PayPal page automation');
|
events.push({ type: 'fetch', url });
|
||||||
|
assert.equal(url, 'https://www.meiguodizhi.com/api/v1/dz');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
address: {
|
||||||
|
Address: '1 Main St',
|
||||||
|
City: 'New York',
|
||||||
|
State: 'New York',
|
||||||
|
Zip_Code: '10001',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getState: async () => {
|
||||||
|
events.push({ type: 'get-state' });
|
||||||
|
return {
|
||||||
|
hostedCheckoutPhoneNumber: '4155551234',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
registerTab: async (source, tabId) => {
|
registerTab: async (source, tabId) => {
|
||||||
events.push({ type: 'register', source, tabId });
|
events.push({ type: 'register', source, tabId });
|
||||||
@@ -332,6 +351,10 @@ test('PayPal no-card binding create only opens hosted checkout and leaves page s
|
|||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP') {
|
||||||
|
currentUrl = 'https://www.paypal.com/pay?token=BA-hosted';
|
||||||
|
return { clicked: true };
|
||||||
|
}
|
||||||
throw new Error(`unexpected message type ${message.type}`);
|
throw new Error(`unexpected message type ${message.type}`);
|
||||||
},
|
},
|
||||||
setState: async (payload) => {
|
setState: async (payload) => {
|
||||||
@@ -358,13 +381,17 @@ test('PayPal no-card binding create only opens hosted checkout and leaves page s
|
|||||||
events.find((event) => event.type === 'tab-update')?.payload?.url,
|
events.find((event) => event.type === 'tab-update')?.payload?.url,
|
||||||
'https://pay.openai.com/c/pay/cs_hosted'
|
'https://pay.openai.com/c/pay/cs_hosted'
|
||||||
);
|
);
|
||||||
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
|
const statePayload = events.filter((event) => event.type === 'set-state').at(-1)?.payload || {};
|
||||||
assert.equal(statePayload.plusCheckoutSource, 'paypal-hosted');
|
assert.equal(statePayload.plusCheckoutSource, 'paypal-hosted');
|
||||||
assert.equal(statePayload.plusCheckoutCountry, 'US');
|
assert.equal(statePayload.plusCheckoutCountry, 'US');
|
||||||
assert.equal(statePayload.plusCheckoutCurrency, 'USD');
|
assert.equal(statePayload.plusCheckoutCurrency, 'USD');
|
||||||
assert.equal(statePayload.plusReturnUrl, '');
|
assert.equal(statePayload.plusReturnUrl, '');
|
||||||
assert.equal(events.some((event) => event.type === 'tab-message' && event.message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'), false);
|
assert.equal(events.some((event) => event.type === 'tab-message' && event.message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'), false);
|
||||||
assert.equal(events.some((event) => event.type === 'tab-message' && event.message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP'), false);
|
assert.equal(events.some((event) => event.type === 'tab-message' && event.message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP'), true);
|
||||||
|
assert.equal(
|
||||||
|
events.find((event) => event.type === 'tab-message' && event.message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP')?.message?.payload?.address?.street,
|
||||||
|
'1 Main St'
|
||||||
|
);
|
||||||
assert.deepStrictEqual(events.find((event) => event.type === 'complete'), {
|
assert.deepStrictEqual(events.find((event) => event.type === 'complete'), {
|
||||||
type: 'complete',
|
type: 'complete',
|
||||||
step: 'plus-checkout-create',
|
step: 'plus-checkout-create',
|
||||||
@@ -372,7 +399,7 @@ test('PayPal no-card binding create only opens hosted checkout and leaves page s
|
|||||||
plusCheckoutCountry: 'US',
|
plusCheckoutCountry: 'US',
|
||||||
plusCheckoutCurrency: 'USD',
|
plusCheckoutCurrency: 'USD',
|
||||||
plusCheckoutSource: 'paypal-hosted',
|
plusCheckoutSource: 'paypal-hosted',
|
||||||
plusCheckoutUrl: 'https://pay.openai.com/c/pay/cs_hosted',
|
plusCheckoutUrl: 'https://www.paypal.com/pay?token=BA-hosted',
|
||||||
plusReturnUrl: '',
|
plusReturnUrl: '',
|
||||||
plusHostedCheckoutCompleted: false,
|
plusHostedCheckoutCompleted: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
|||||||
'fetch-signup-code',
|
'fetch-signup-code',
|
||||||
'fill-profile',
|
'fill-profile',
|
||||||
'plus-checkout-create',
|
'plus-checkout-create',
|
||||||
'paypal-hosted-openai-checkout',
|
|
||||||
'paypal-hosted-email',
|
'paypal-hosted-email',
|
||||||
'paypal-hosted-verification',
|
'paypal-hosted-verification',
|
||||||
'paypal-hosted-card',
|
'paypal-hosted-card',
|
||||||
@@ -236,10 +235,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
|||||||
assert.equal(hostedSteps.some((step) => step.key === 'plus-checkout-billing'), false);
|
assert.equal(hostedSteps.some((step) => step.key === 'plus-checkout-billing'), false);
|
||||||
assert.equal(hostedSteps.some((step) => step.key === 'paypal-approve'), false);
|
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 === 'plus-checkout-return'), false);
|
||||||
assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-openai-checkout')?.title, '无卡直绑提交 OpenAI Checkout');
|
assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-openai-checkout'), false);
|
||||||
assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-card')?.title, '无卡直绑填写 PayPal 资料');
|
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, 16]);
|
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' }), 16);
|
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 15);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
goPaySteps.map((step) => step.key),
|
goPaySteps.map((step) => step.key),
|
||||||
@@ -317,7 +316,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
|||||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||||
},
|
},
|
||||||
previousNodeId: 'paypal-hosted-review',
|
previousNodeId: 'paypal-hosted-review',
|
||||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'gopay',
|
label: 'gopay',
|
||||||
@@ -408,7 +407,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
|||||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||||
},
|
},
|
||||||
previousNodeId: 'paypal-hosted-review',
|
previousNodeId: 'paypal-hosted-review',
|
||||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'gopay',
|
label: 'gopay',
|
||||||
|
|||||||
Reference in New Issue
Block a user