Add independent PayPal hosted binding mode
This commit is contained in:
@@ -3,6 +3,14 @@
|
||||
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
|
||||
|
||||
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
|
||||
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_REVIEW = 'review_consent';
|
||||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||||
|
||||
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
|
||||
@@ -13,6 +21,8 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1'
|
||||
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|
||||
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|
||||
|| message.type === 'PAYPAL_CLICK_APPROVE'
|
||||
|| message.type === 'PAYPAL_HOSTED_GET_STATE'
|
||||
|| message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
|
||||
) {
|
||||
resetStopState();
|
||||
handlePayPalCommand(message).then((result) => {
|
||||
@@ -47,6 +57,10 @@ async function handlePayPalCommand(message) {
|
||||
return dismissPayPalPrompts();
|
||||
case 'PAYPAL_CLICK_APPROVE':
|
||||
return clickPayPalApprove();
|
||||
case 'PAYPAL_HOSTED_GET_STATE':
|
||||
return inspectPayPalHostedState();
|
||||
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
|
||||
return runPayPalHostedCheckoutStep(message.payload || {});
|
||||
default:
|
||||
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
@@ -223,6 +237,302 @@ function findApproveButton() {
|
||||
]);
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
function isHostedGuestCheckoutPage() {
|
||||
return /\/checkoutweb\//i.test(getPayPalPathname())
|
||||
|| Boolean(document.getElementById('cardNumber'))
|
||||
|| Boolean(document.getElementById('billingLine1'));
|
||||
}
|
||||
|
||||
function isHostedReviewPage() {
|
||||
return /\/webapps\/hermes/i.test(getPayPalPathname());
|
||||
}
|
||||
|
||||
function findHostedReviewConsentButton() {
|
||||
const direct = document.getElementById('consentButton')
|
||||
|| document.querySelector('button[data-testid="consentButton"]');
|
||||
if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
|
||||
return direct;
|
||||
}
|
||||
return findClickableByText([
|
||||
/agree\s*(?:and)?\s*continue|accept|continue/i,
|
||||
/同意并继续|同意|继续/i,
|
||||
]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (isHostedReviewPage() && findHostedReviewConsentButton()) {
|
||||
return PAYPAL_HOSTED_STAGE_REVIEW;
|
||||
}
|
||||
if (isHostedLoginPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_LOGIN;
|
||||
}
|
||||
return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
|
||||
}
|
||||
|
||||
function fillHostedInputById(id, value) {
|
||||
const input = document.getElementById(String(id || '').trim());
|
||||
if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
|
||||
return false;
|
||||
}
|
||||
fillInput(input, String(value || ''));
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectHostedOptionByIdText(id, value) {
|
||||
const select = document.getElementById(String(id || '').trim());
|
||||
const expected = normalizeText(value).toLowerCase();
|
||||
if (!select || !expected) {
|
||||
return false;
|
||||
}
|
||||
const option = Array.from(select.options || []).find((item) => {
|
||||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||||
return optionText.includes(expected) || optionValue.includes(expected);
|
||||
});
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function findHostedSubmitButton() {
|
||||
return document.querySelector('button[data-testid="submit-button"]')
|
||||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||||
|| document.querySelector('button.SubmitButton--complete')
|
||||
|| findEmailNextButton()
|
||||
|| findLoginNextButton()
|
||||
|| findClickableByText([
|
||||
/pay|continue|next|agree|subscribe/i,
|
||||
/支付|继续|下一步|同意|订阅/i,
|
||||
]);
|
||||
}
|
||||
|
||||
async function clickHostedSubmitButton() {
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedSubmitButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-submit' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
await sleep(1000);
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
verificationRequired: hasHostedVerificationInputs(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostedRandomEmail() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let value = '';
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return `${value}@gmail.com`;
|
||||
}
|
||||
|
||||
function buildHostedRandomPassword() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
|
||||
let value = 'Aa1!';
|
||||
while (value.length < 14) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildHostedVisaCard() {
|
||||
const digits = [4, 1, 4, 7];
|
||||
while (digits.length < 15) {
|
||||
digits.push(Math.floor(Math.random() * 10));
|
||||
}
|
||||
const reversed = digits.slice().reverse();
|
||||
let sum = 0;
|
||||
for (let index = 0; index < reversed.length; index += 1) {
|
||||
let digit = reversed[index];
|
||||
if (index % 2 === 0) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
sum += digit;
|
||||
}
|
||||
digits.push((10 - (sum % 10)) % 10);
|
||||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||||
const year = (new Date().getFullYear() % 100) + 3;
|
||||
return {
|
||||
number: digits.join(''),
|
||||
expiry: `${month} / ${year}`,
|
||||
cvv: String(Math.floor(100 + Math.random() * 900)),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitHostedLogin(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const email = normalizeText(payload.email || buildHostedRandomEmail());
|
||||
const emailInput = document.getElementById('email') || findEmailInput();
|
||||
if (!emailInput) {
|
||||
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
||||
}
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const clickResult = await clickHostedSubmitButton();
|
||||
return {
|
||||
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: 'plus-checkout-create', kind: 'fill', label: 'hosted-paypal-verification-code' }, async () => {
|
||||
inputs.forEach((input, index) => fillInput(input, code[index] || ''));
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_VERIFICATION,
|
||||
codeSubmitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function fillHostedGuestCheckout(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const countrySelect = document.getElementById('country');
|
||||
if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
|
||||
countrySelect.value = 'US';
|
||||
countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await sleep(1000);
|
||||
}
|
||||
const generatedCard = buildHostedVisaCard();
|
||||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||||
const values = {
|
||||
email: normalizeText(payload.email || buildHostedRandomEmail()),
|
||||
phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
|
||||
cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
|
||||
cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
|
||||
cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
|
||||
password: String(payload.password || buildHostedRandomPassword()),
|
||||
firstName: normalizeText(payload.firstName || 'James'),
|
||||
lastName: normalizeText(payload.lastName || 'Smith'),
|
||||
};
|
||||
fillHostedInputById('email', values.email);
|
||||
fillHostedInputById('phone', values.phone);
|
||||
fillHostedInputById('cardNumber', values.cardNumber);
|
||||
fillHostedInputById('cardExpiry', values.cardExpiry);
|
||||
fillHostedInputById('cardCvv', values.cardCvv);
|
||||
fillHostedInputById('password', values.password);
|
||||
fillHostedInputById('firstName', values.firstName);
|
||||
fillHostedInputById('lastName', values.lastName);
|
||||
fillHostedInputById('billingLine1', address.street || address.address1 || '');
|
||||
fillHostedInputById('billingCity', address.city || '');
|
||||
fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
|
||||
selectHostedOptionByIdText('billingState', address.state || address.region || '');
|
||||
const clickResult = await clickHostedSubmitButton();
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||||
submitted: true,
|
||||
verificationRequired: Boolean(clickResult.verificationRequired),
|
||||
};
|
||||
}
|
||||
|
||||
async function clickHostedReviewConsent() {
|
||||
await waitForDocumentComplete();
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedReviewConsentButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 30000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-review-consent' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_REVIEW,
|
||||
submitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPayPalHostedCheckoutStep(payload = {}) {
|
||||
const stage = detectPayPalHostedStage();
|
||||
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);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
|
||||
return fillHostedGuestCheckout(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
||||
return clickHostedReviewConsent();
|
||||
}
|
||||
return {
|
||||
stage,
|
||||
submitted: false,
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
};
|
||||
}
|
||||
|
||||
function inspectPayPalHostedState() {
|
||||
const stage = detectPayPalHostedStage();
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
hostedStage: stage,
|
||||
verificationInputsVisible: hasHostedVerificationInputs(),
|
||||
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
||||
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
||||
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
function findPasskeyPromptButtons() {
|
||||
const promptPatterns = [
|
||||
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
|
||||
|
||||
+206
-2
@@ -34,6 +34,7 @@ const PLUS_CHECKOUT_CONFIGS = {
|
||||
};
|
||||
const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000;
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
@@ -47,6 +48,17 @@ const PAYMENT_METHOD_CONFIGS = {
|
||||
},
|
||||
patterns: [/paypal/i],
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL_HOSTED]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||
label: 'PayPal 无卡直绑',
|
||||
diagnosticLabel: 'PayPal hosted',
|
||||
checkoutMerchantPath: 'openai_llc',
|
||||
billingDetails: {
|
||||
country: 'US',
|
||||
currency: 'USD',
|
||||
},
|
||||
patterns: [/paypal/i],
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_GOPAY]: {
|
||||
id: PLUS_PAYMENT_METHOD_GOPAY,
|
||||
label: 'GoPay',
|
||||
@@ -80,6 +92,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION'
|
||||
|| message.type === 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS'
|
||||
|| message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE'
|
||||
|| message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP'
|
||||
|| message.type === 'PLUS_CHECKOUT_GET_STATE'
|
||||
) {
|
||||
resetStopState();
|
||||
@@ -119,6 +132,8 @@ async function handlePlusCheckoutCommand(message) {
|
||||
return ensurePlusStructuredBillingAddress(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE':
|
||||
return clickPlusSubscribe(message.payload || {});
|
||||
case 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP':
|
||||
return runPayPalHostedOpenAiCheckoutStep(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_GET_STATE':
|
||||
return inspectPlusCheckoutState(message.payload || {});
|
||||
default:
|
||||
@@ -152,6 +167,153 @@ async function waitForDocumentComplete() {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
function isPayPalHostedOpenAiCheckoutPage() {
|
||||
const host = String(location?.host || '').toLowerCase();
|
||||
return host.includes('pay.openai.com') || host.includes('checkout.stripe.com');
|
||||
}
|
||||
|
||||
function hideHostedAddressAutocomplete() {
|
||||
[
|
||||
'.AddressAutocomplete-results',
|
||||
'[class*="AddressAutocomplete"]',
|
||||
'#billing-address-autocomplete-results',
|
||||
].forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((node) => {
|
||||
try {
|
||||
node.style.setProperty('display', 'none', 'important');
|
||||
node.style.setProperty('visibility', 'hidden', 'important');
|
||||
node.style.setProperty('pointer-events', 'none', 'important');
|
||||
} catch {
|
||||
// Best effort only; checkout still works without this cleanup.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hasHostedOpenAiVerificationDialog() {
|
||||
return Boolean(document.getElementById('ci-ciBasic-0'));
|
||||
}
|
||||
|
||||
function fillHostedInput(selector, value) {
|
||||
const input = document.querySelector(selector);
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
fillInput(input, String(value || ''));
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectHostedOptionByText(selector, value) {
|
||||
const select = document.querySelector(selector);
|
||||
const expected = normalizeText(value).toLowerCase();
|
||||
if (!select || !expected) {
|
||||
return false;
|
||||
}
|
||||
const option = Array.from(select.options || []).find((item) => {
|
||||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||||
return optionText.includes(expected) || optionValue.includes(expected);
|
||||
});
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function findHostedPayPalButton() {
|
||||
return document.querySelector('[data-testid="paypal-accordion-item-button"]')
|
||||
|| document.querySelector('.paypal-accordion-item button')
|
||||
|| findClickableByText([/paypal/i]);
|
||||
}
|
||||
|
||||
function findHostedSubmitButton() {
|
||||
return document.querySelector('button[data-testid="submit-button"]')
|
||||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||||
|| document.querySelector('button.SubmitButton--complete')
|
||||
|| findClickableByText([
|
||||
/next|continue|pay|subscribe|agree/i,
|
||||
/下一页|继续|支付|订阅|同意/i,
|
||||
]);
|
||||
}
|
||||
|
||||
async function clickHostedSubmitButton() {
|
||||
const button = await waitUntil(() => {
|
||||
hideHostedAddressAutocomplete();
|
||||
const candidate = findHostedSubmitButton();
|
||||
return candidate && isEnabledControl(candidate) && isVisibleElement(candidate) ? candidate : null;
|
||||
}, {
|
||||
label: 'hosted checkout 提交按钮',
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
document.activeElement?.blur?.();
|
||||
await sleep(300);
|
||||
simulateClick(button);
|
||||
await sleep(1200);
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
||||
};
|
||||
}
|
||||
|
||||
function fillHostedOpenAiVerificationCode(verificationCode = '') {
|
||||
const code = String(verificationCode || '').replace(/\D+/g, '').slice(0, 6);
|
||||
if (code.length !== 6) {
|
||||
throw new Error('hosted checkout OpenAI 验证码无效。');
|
||||
}
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
const input = document.getElementById(`ci-ciBasic-${index}`);
|
||||
if (!input) {
|
||||
throw new Error('hosted checkout OpenAI 页面未找到完整的验证码输入框。');
|
||||
}
|
||||
fillInput(input, code[index]);
|
||||
}
|
||||
return {
|
||||
verificationCodeFilled: true,
|
||||
hostedVerificationVisible: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPayPalHostedOpenAiCheckoutStep(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
if (!isPayPalHostedOpenAiCheckoutPage()) {
|
||||
throw new Error('当前页面不是 PayPal 无卡直绑的 OpenAI hosted checkout 页面。');
|
||||
}
|
||||
if (payload.verificationCode) {
|
||||
return fillHostedOpenAiVerificationCode(payload.verificationCode);
|
||||
}
|
||||
|
||||
hideHostedAddressAutocomplete();
|
||||
const payPalButton = findHostedPayPalButton();
|
||||
if (payPalButton) {
|
||||
simulateClick(payPalButton);
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||||
fillHostedInput('#billingAddressLine1', address.street || address.address1 || '');
|
||||
fillHostedInput('#billingLocality', address.city || '');
|
||||
fillHostedInput('#billingPostalCode', address.zip || address.postalCode || '');
|
||||
selectHostedOptionByText('#billingAdministrativeArea', address.state || address.region || '');
|
||||
|
||||
const consent = document.getElementById('termsOfServiceConsentCheckbox');
|
||||
if (consent && !consent.checked) {
|
||||
simulateClick(consent);
|
||||
}
|
||||
|
||||
for (let count = 0; count < 5; count += 1) {
|
||||
hideHostedAddressAutocomplete();
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return clickHostedSubmitButton();
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
@@ -423,7 +585,14 @@ function findPaymentCardAncestor(el, pattern) {
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
@@ -620,6 +789,7 @@ function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(paymentMethod);
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
|
||||
checkout_ui_mode: config.id === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED ? 'hosted' : 'custom',
|
||||
billing_details: {
|
||||
...config.billingDetails,
|
||||
},
|
||||
@@ -635,6 +805,29 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
|
||||
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
|
||||
}
|
||||
|
||||
function findHostedCheckoutUrl(payload = {}) {
|
||||
const queue = [payload];
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (!current || typeof current !== 'object') {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
queue.push(...current);
|
||||
continue;
|
||||
}
|
||||
for (const value of Object.values(current)) {
|
||||
if (typeof value === 'string' && /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay\//i.test(value.trim())) {
|
||||
return value.trim();
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
queue.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function createPlusCheckoutSession(options = {}) {
|
||||
await waitForDocumentComplete();
|
||||
log('Plus:正在读取 ChatGPT 登录会话...');
|
||||
@@ -667,8 +860,16 @@ async function createPlusCheckoutSession(options = {}) {
|
||||
throw new Error(`创建 Plus Checkout 失败:${detail}`);
|
||||
}
|
||||
|
||||
const checkoutUrl = buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod);
|
||||
const hostedCheckoutUrl = findHostedCheckoutUrl(data);
|
||||
const preferredCheckoutUrl = paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
? (hostedCheckoutUrl || checkoutUrl)
|
||||
: checkoutUrl;
|
||||
|
||||
return {
|
||||
checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
|
||||
checkoutUrl,
|
||||
hostedCheckoutUrl,
|
||||
preferredCheckoutUrl,
|
||||
country: checkoutPayload.billing_details.country,
|
||||
currency: checkoutPayload.billing_details.currency,
|
||||
};
|
||||
@@ -1550,6 +1751,9 @@ async function inspectPlusCheckoutState(options = {}) {
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
hasSubscribeButton: Boolean(findSubscribeButton()),
|
||||
hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(),
|
||||
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
||||
hostedPayPalButtonFound: Boolean(findHostedPayPalButton()),
|
||||
checkoutAmountSummary: getCheckoutAmountSummary(),
|
||||
addressFieldValues: {
|
||||
address1: structuredAddress.address1?.value || '',
|
||||
|
||||
Reference in New Issue
Block a user