From 72c578b9b499bc1117e2f89388a2f8a8a66270aa Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 20 May 2026 09:03:40 +0800 Subject: [PATCH] Add independent PayPal hosted binding mode --- background.js | 89 +++- background/steps/create-plus-checkout.js | 471 +++++++++++++++++- content/paypal-flow.js | 310 ++++++++++++ content/plus-checkout.js | 208 +++++++- data/step-definitions.js | 93 ++++ gopay-utils.js | 5 + shared/flow-capabilities.js | 21 +- shared/settings-schema.js | 26 +- sidepanel/sidepanel.html | 17 + sidepanel/sidepanel.js | 126 ++++- ...ackground-account-history-settings.test.js | 1 + tests/flow-capabilities-module.test.js | 39 +- tests/gopay-utils.test.js | 2 + tests/plus-account-access-strategy.test.js | 20 +- tests/plus-checkout-address-input.test.js | 38 +- tests/plus-checkout-create-wait.test.js | 107 ++++ tests/sidepanel-flow-source-registry.test.js | 1 + tests/sidepanel-plus-payment-method.test.js | 65 ++- tests/step-definitions-module.test.js | 42 ++ 19 files changed, 1621 insertions(+), 60 deletions(-) diff --git a/background.js b/background.js index d826c65..dc315cd 100644 --- a/background.js +++ b/background.js @@ -128,6 +128,36 @@ const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageSte signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true, }) || PLUS_PAYPAL_PHONE_STEP_DEFINITIONS; +const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal-hosted', +}) || PLUS_PAYPAL_STEP_DEFINITIONS; +const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal-hosted', + plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, +}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS; +const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal-hosted', + plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION, +}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS; +const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal-hosted', + signupMethod: 'phone', +}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS; +const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + activeFlowId: DEFAULT_ACTIVE_FLOW_ID, + plusModeEnabled: true, + plusPaymentMethod: 'paypal-hosted', + signupMethod: 'phone', + phoneSignupReloginAfterBindEmailEnabled: true, +}) || PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS; const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ activeFlowId: DEFAULT_ACTIVE_FLOW_ID, plusModeEnabled: true, @@ -214,6 +244,11 @@ const ALL_STEP_DEFINITIONS = (() => { ...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, ...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS, @@ -243,6 +278,10 @@ const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); +const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS = PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS + .map((definition) => Number(definition?.id)) + .filter(Number.isFinite) + .sort((left, right) => left - right); const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) @@ -255,6 +294,7 @@ const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS; const LAST_STEP_ID = Math.max( NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10, + PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS[PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS.length - 1] || 10, PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10, PLUS_GPC_STEP_IDS[PLUS_GPC_STEP_IDS.length - 1] || 10 ); @@ -681,9 +721,11 @@ const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ ]); const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; +const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; -const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL; +const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED; +const DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS = 3; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; const PERSISTENT_ALIAS_STATE_KEYS = [ @@ -781,6 +823,12 @@ function isPlusModeState(state = {}) { function normalizePlusPaymentMethod(value = '') { 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; + } if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_PAYMENT_METHOD_GPC_HELPER; } @@ -937,6 +985,20 @@ function getStepDefinitionsForState(state = {}) { } return PLUS_GOPAY_STEP_DEFINITIONS; } + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + if (signupMethod === SIGNUP_METHOD_PHONE) { + return Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled) + ? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS + : PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS; + } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { + return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS; + } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) { + return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS; + } + return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS; + } if ( signupMethod === SIGNUP_METHOD_EMAIL && plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION @@ -967,6 +1029,9 @@ function getStepIdsForState(state = {}) { if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_GPC_STEP_IDS; } + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS; + } return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS; } @@ -1151,6 +1216,9 @@ const PERSISTED_SETTING_DEFAULTS = { plusModeEnabled: false, plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD, plusAccountAccessStrategy: 'oauth', + hostedCheckoutVerificationUrl: '', + hostedCheckoutPhoneNumber: '', + plusHostedCheckoutOauthDelaySeconds: DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS, paypalEmail: '', paypalPassword: '', currentPayPalAccountId: '', @@ -1325,6 +1393,9 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([ 'plusModeEnabled', 'plusPaymentMethod', 'plusAccountAccessStrategy', + 'hostedCheckoutVerificationUrl', + 'hostedCheckoutPhoneNumber', + 'plusHostedCheckoutOauthDelaySeconds', 'mailProvider', 'ipProxyEnabled', 'ipProxyService', @@ -1892,6 +1963,12 @@ function normalizePlusPaymentMethod(value = '') { return rootScope.GoPayUtils.normalizePlusPaymentMethod(value); } 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; + } if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_PAYMENT_METHOD_GPC_HELPER; } @@ -3084,6 +3161,14 @@ function normalizePersistentSettingValue(key, value) { return normalizePlusPaymentMethod(value); case 'plusAccountAccessStrategy': return normalizePlusAccountAccessStrategy(value); + case 'hostedCheckoutVerificationUrl': + return String(value || '').trim(); + case 'hostedCheckoutPhoneNumber': + return String(value || '').trim(); + case 'plusHostedCheckoutOauthDelaySeconds': { + const numeric = Number(value); + return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS))); + } case 'paypalEmail': return String(value || '').trim(); case 'paypalPassword': @@ -13358,6 +13443,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c createAutomationTab, ensureContentScriptReadyOnTabUntilStopped, fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState, markCurrentRegistrationAccountUsed, registerTab, sendTabMessageUntilStopped, @@ -13365,6 +13451,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c sleepWithStop, throwIfStopped, waitForTabCompleteUntilStopped, + waitForTabUrlMatchUntilStopped, }); const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({ addLog, diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 3e5fb30..e3b1f71 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -2,14 +2,24 @@ root.MultiPageBackgroundPlusCheckoutCreate = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutCreateModule() { const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; + const PAYPAL_SOURCE = 'paypal-flow'; const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/'; const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js']; + const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/paypal-flow.js']; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; + const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; + const HOSTED_CHECKOUT_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz'; + const HOSTED_CHECKOUT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i; + const HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS = 120000; + const HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS = 10 * 60 * 1000; + const HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS = 12; + const HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS = 5000; + const HOSTED_CHECKOUT_DEFAULT_PHONE = '1234567890'; function createPlusCheckoutCreateExecutor(deps = {}) { const { @@ -19,11 +29,13 @@ createAutomationTab = null, ensureContentScriptReadyOnTabUntilStopped, fetch: fetchImpl = null, + getState = null, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, + waitForTabUrlMatchUntilStopped = null, throwIfStopped = () => {}, } = deps; @@ -41,6 +53,9 @@ return rootScope.GoPayUtils.normalizePlusPaymentMethod(value); } const normalized = String(value || '').trim().toLowerCase(); + if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') { + return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED; + } if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_PAYMENT_METHOD_GPC_HELPER; } @@ -52,6 +67,9 @@ if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { return 'GPC 订阅页'; } + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + return 'PayPal 无卡直绑'; + } return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout'; } @@ -60,6 +78,9 @@ if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { return 'GPC'; } + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + return 'PayPal 无卡直绑'; + } return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal'; } @@ -77,6 +98,444 @@ return tabId; } + function isPayPalUrl(url = '') { + return /paypal\./i.test(String(url || '')); + } + + function isHostedCheckoutSuccessUrl(url = '') { + return HOSTED_CHECKOUT_SUCCESS_URL_PATTERN.test(String(url || '')); + } + + function isHostedOpenAiCheckoutUrl(url = '') { + return /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay(?:\/|$)/i.test(String(url || '')); + } + + async function waitForUrlMatch(tabId, matcher, timeoutMs = 30000, retryDelayMs = 500) { + const deadline = Date.now() + Math.max(1000, Number(timeoutMs) || 30000); + while (Date.now() < deadline) { + throwIfStopped(); + const tab = await chrome?.tabs?.get?.(tabId).catch(() => null); + if (!tab) { + return null; + } + if (matcher(tab.url || '', tab)) { + return tab; + } + await sleepWithStop(retryDelayMs); + } + return null; + } + + async function getHostedCheckoutRuntimeConfig(state = {}) { + const latestState = typeof getState === 'function' ? await getState().catch(() => ({})) : {}; + return { + verificationUrl: String( + state?.hostedCheckoutVerificationUrl + || latestState?.hostedCheckoutVerificationUrl + || '' + ).trim(), + phone: String( + state?.hostedCheckoutPhoneNumber + || latestState?.hostedCheckoutPhoneNumber + || HOSTED_CHECKOUT_DEFAULT_PHONE + ).trim(), + oauthDelaySeconds: normalizeHostedCheckoutDelaySeconds( + state?.plusHostedCheckoutOauthDelaySeconds + ?? latestState?.plusHostedCheckoutOauthDelaySeconds + ), + }; + } + + function normalizeHostedCheckoutDelaySeconds(value) { + const numeric = Number(value); + if (!Number.isFinite(numeric)) { + return 3; + } + return Math.min(120, Math.max(0, Math.floor(numeric))); + } + + async function fetchHostedCheckoutAddress() { + const { response, data } = await fetchJsonWithTimeout(HOSTED_CHECKOUT_ADDRESS_ENDPOINT, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ path: '/', method: 'address' }), + }, 30000); + if (!response?.ok) { + throw new Error(`获取无卡直绑地址失败(HTTP ${response?.status || 0})。`); + } + const address = data?.address || data || {}; + return { + street: String(address.Address || address.street || '123 Main St').trim(), + city: String(address.City || address.city || 'New York').trim(), + state: String(address.State_Full || address.State || address.state || 'New York').trim(), + zip: String(address.Zip_Code || address.zip || '10001').trim().slice(0, 5) || '10001', + }; + } + + function buildRandomHostedEmail() { + const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let localPart = ''; + for (let index = 0; index < 16; index += 1) { + localPart += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return `${localPart}@gmail.com`; + } + + function buildRandomHostedPassword() { + 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)), + }; + } + + function buildHostedGuestProfile(address = {}, config = {}) { + const card = buildHostedVisaCard(); + return { + email: buildRandomHostedEmail(), + password: buildRandomHostedPassword(), + phone: String(config?.phone || HOSTED_CHECKOUT_DEFAULT_PHONE).trim(), + firstName: 'James', + lastName: 'Smith', + cardNumber: card.number, + cardExpiry: card.expiry, + cardCvv: card.cvv, + address, + }; + } + + function extractHostedVerificationCode(payload = '') { + const candidates = [payload?.data, payload?.code, payload?.text, payload?.message, payload]; + for (const candidate of candidates) { + const text = typeof candidate === 'object' ? JSON.stringify(candidate) : String(candidate || ''); + const match = text.match(/\d{6}/); + if (match) { + return match[0]; + } + } + return ''; + } + + async function fetchHostedVerificationCode(verificationUrl = '') { + const url = String(verificationUrl || '').trim(); + if (!url) { + throw new Error('未配置 PayPal 无卡直绑验证码接口。'); + } + const fetcher = typeof fetchImpl === 'function' + ? fetchImpl + : (typeof fetch === 'function' ? fetch.bind(globalThis) : null); + if (typeof fetcher !== 'function') { + throw new Error('当前运行环境不支持 fetch,无法获取无卡直绑验证码。'); + } + const separator = url.includes('?') ? '&' : '?'; + const response = await fetcher(`${url}${separator}t=${Date.now()}`, { + method: 'GET', + headers: { Accept: 'application/json,text/plain,*/*' }, + }); + const text = await response.text().catch(() => ''); + let payload = text; + try { + payload = text ? JSON.parse(text) : {}; + } catch { + payload = text; + } + const code = extractHostedVerificationCode(payload); + if (!code) { + throw new Error('验证码接口暂未返回有效 6 位验证码。'); + } + return code; + } + + async function pollHostedVerificationCode(verificationUrl = '') { + let lastError = null; + for (let attempt = 1; attempt <= HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS; attempt += 1) { + throwIfStopped(); + try { + const code = await fetchHostedVerificationCode(verificationUrl); + await addLog(`步骤 6:已获取无卡直绑验证码(${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'); + if (attempt < HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS) { + await sleepWithStop(HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS); + } + } + } + throw lastError || new Error('无卡直绑验证码轮询失败。'); + } + + async function runHostedOpenAiCheckout(tabId, profile, config) { + await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { + inject: PLUS_CHECKOUT_INJECT_FILES, + injectSource: PLUS_CHECKOUT_SOURCE, + logMessage: '步骤 6:正在等待 OpenAI hosted checkout 脚本就绪...', + }); + const firstResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { + type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP', + source: 'background', + payload: { address: profile.address }, + }); + if (firstResult?.error) { + throw new Error(firstResult.error); + } + + const deadline = Date.now() + HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS; + let verificationSubmitted = false; + while (Date.now() < deadline) { + throwIfStopped(); + const tab = await chrome?.tabs?.get?.(tabId).catch(() => null); + if (!tab) { + throw new Error('步骤 6:无卡直绑 checkout 标签页已关闭。'); + } + const currentUrl = String(tab.url || '').trim(); + if (isPayPalUrl(currentUrl) || isHostedCheckoutSuccessUrl(currentUrl)) { + return currentUrl; + } + await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { + inject: PLUS_CHECKOUT_INJECT_FILES, + injectSource: PLUS_CHECKOUT_SOURCE, + }); + const pageState = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { + type: 'PLUS_CHECKOUT_GET_STATE', + source: 'background', + payload: {}, + }); + if (pageState?.error) { + throw new Error(pageState.error); + } + if (pageState?.hostedVerificationVisible && !verificationSubmitted) { + const verificationCode = await pollHostedVerificationCode(config.verificationUrl); + const verifyResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { + type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP', + source: 'background', + payload: { verificationCode }, + }); + if (verifyResult?.error) { + throw new Error(verifyResult.error); + } + verificationSubmitted = true; + } + await sleepWithStop(500); + } + throw new Error('步骤 6:OpenAI hosted checkout 长时间未跳转到 PayPal 或支付成功页。'); + } + + async function getHostedPayPalState(tabId) { + await waitForTabCompleteUntilStopped(tabId); + await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, { + inject: PAYPAL_INJECT_FILES, + injectSource: PAYPAL_SOURCE, + logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...', + }); + const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, { + type: 'PAYPAL_HOSTED_GET_STATE', + source: 'background', + payload: {}, + }); + if (result?.error) { + throw new Error(result.error); + } + return result || {}; + } + + async function runHostedPayPalStep(tabId, payload = {}) { + await waitForTabCompleteUntilStopped(tabId); + await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, { + inject: PAYPAL_INJECT_FILES, + injectSource: PAYPAL_SOURCE, + logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...', + }); + const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, { + type: 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP', + source: 'background', + payload, + }); + if (result?.error) { + throw new Error(result.error); + } + return result || {}; + } + + async function runHostedPayPalCheckout(tabId, profile, config) { + const deadline = Date.now() + HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS; + while (Date.now() < deadline) { + throwIfStopped(); + const tab = await chrome?.tabs?.get?.(tabId).catch(() => null); + if (!tab) { + throw new Error('步骤 6:PayPal 无卡直绑标签页已关闭。'); + } + const currentUrl = String(tab.url || '').trim(); + if (isHostedCheckoutSuccessUrl(currentUrl)) { + await addLog('步骤 6:PayPal 无卡直绑已回到 ChatGPT 支付成功页。', 'ok'); + return currentUrl; + } + if (!isPayPalUrl(currentUrl)) { + await addLog(`步骤 6:无卡直绑已离开 PayPal(${currentUrl || '空 URL'}),继续等待支付成功页。`, 'info'); + await sleepWithStop(1000); + continue; + } + + const pageState = await getHostedPayPalState(tabId); + if (pageState.hostedStage === 'verification' && pageState.verificationInputsVisible) { + const verificationCode = await pollHostedVerificationCode(config.verificationUrl); + await runHostedPayPalStep(tabId, { verificationCode }); + await sleepWithStop(1000); + continue; + } + if (pageState.hostedStage === 'pay_login') { + await addLog('步骤 6:正在填写 PayPal 无卡直绑登录邮箱。', 'info'); + await runHostedPayPalStep(tabId, { email: profile.email }); + await sleepWithStop(1000); + continue; + } + if (pageState.hostedStage === 'guest_checkout') { + await addLog('步骤 6:正在填写 PayPal 无卡直绑卡支付资料。', 'info'); + await runHostedPayPalStep(tabId, { + ...profile, + phone: config.phone || profile.phone, + }); + await sleepWithStop(1500); + continue; + } + if (pageState.hostedStage === 'review_consent') { + await addLog('步骤 6:正在确认 PayPal 无卡直绑账单复核页。', 'info'); + await runHostedPayPalStep(tabId, {}); + await sleepWithStop(1000); + continue; + } + if (pageState.hostedStage === 'approval') { + throw new Error('步骤 6:无卡直绑流程进入普通 PayPal 授权页,请检查支付方式是否选择正确。'); + } + await sleepWithStop(1000); + } + throw new Error('步骤 6:PayPal 无卡直绑自动化超时,未等到支付成功页。'); + } + + async function waitHostedSuccessDelayAndComplete(tabId, state = {}, successUrl = '') { + const config = await getHostedCheckoutRuntimeConfig(state); + if (config.oauthDelaySeconds > 0) { + await addLog(`步骤 6:支付成功后等待 ${config.oauthDelaySeconds} 秒,再继续账号接入。`, 'info'); + await sleepWithStop(config.oauthDelaySeconds * 1000); + } + await completeNodeFromBackground('plus-checkout-create', { + plusReturnUrl: successUrl, + plusHostedCheckoutCompleted: true, + plusHostedCheckoutOauthDelaySeconds: config.oauthDelaySeconds, + }); + } + + function resolveCheckoutTargetUrl(result = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) { + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + return String( + result?.preferredCheckoutUrl + || result?.hostedCheckoutUrl + || result?.checkoutUrl + || '' + ).trim(); + } + return String(result?.checkoutUrl || '').trim(); + } + + async function executeHostedCheckoutCreate(tabId, state = {}, result = {}) { + const targetCheckoutUrl = resolveCheckoutTargetUrl(result, PLUS_PAYMENT_METHOD_PAYPAL_HOSTED); + if (!targetCheckoutUrl) { + throw new Error('步骤 6:PayPal 无卡直绑未返回可用的订阅链接。'); + } + + await addLog('步骤 6:PayPal 无卡直绑链接已创建,正在打开 hosted checkout 页面...', 'ok'); + await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true }); + await waitForTabCompleteUntilStopped(tabId); + + const landedTab = await waitForUrlMatch( + tabId, + (url) => isHostedOpenAiCheckoutUrl(url) || isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS, + 500 + ); + const landedUrl = String(landedTab?.url || targetCheckoutUrl || '').trim(); + + await setState({ + plusCheckoutTabId: tabId, + plusCheckoutUrl: landedUrl, + plusCheckoutCountry: result.country || 'US', + plusCheckoutCurrency: result.currency || 'USD', + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + plusReturnUrl: '', + }); + + await addLog(`步骤 6:PayPal 无卡直绑页面已就绪(${result.country || 'US'} ${result.currency || 'USD'}),开始自动完成直绑支付链路。`, 'info'); + + if (isHostedCheckoutSuccessUrl(landedUrl)) { + await waitHostedSuccessDelayAndComplete(tabId, state, landedUrl); + return; + } + + const config = await getHostedCheckoutRuntimeConfig(state); + const address = await fetchHostedCheckoutAddress(); + const guestProfile = buildHostedGuestProfile(address, config); + let transitionUrl = landedUrl; + + if (isHostedOpenAiCheckoutUrl(transitionUrl)) { + transitionUrl = await runHostedOpenAiCheckout(tabId, guestProfile, config); + } + + if (isHostedCheckoutSuccessUrl(transitionUrl)) { + await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl); + return; + } + + if (!isPayPalUrl(transitionUrl)) { + const transitionTab = await waitForUrlMatch( + tabId, + (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS, + 500 + ); + transitionUrl = String(transitionTab?.url || '').trim(); + } + + if (isHostedCheckoutSuccessUrl(transitionUrl)) { + await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl); + return; + } + if (!isPayPalUrl(transitionUrl)) { + throw new Error('步骤 6:PayPal 无卡直绑提交后未进入 PayPal 或支付成功页。'); + } + + const successUrl = await runHostedPayPalCheckout(tabId, guestProfile, config); + await waitHostedSuccessDelayAndComplete(tabId, state, successUrl); + } + function normalizeHelperCountryCode(countryCode = '86') { const digits = String(countryCode || '').replace(/\D/g, ''); return digits || '86'; @@ -493,12 +952,18 @@ if (result?.error) { throw new Error(result.error); } - if (!result?.checkoutUrl) { + const targetCheckoutUrl = resolveCheckoutTargetUrl(result, paymentMethod); + if (!targetCheckoutUrl) { throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`); } + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + await executeHostedCheckoutCreate(tabId, state, result); + return; + } + await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok'); - await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true }); + await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true }); await waitForTabCompleteUntilStopped(tabId); await sleepWithStop(1000); await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { @@ -509,7 +974,7 @@ await setState({ plusCheckoutTabId: tabId, - plusCheckoutUrl: result.checkoutUrl, + plusCheckoutUrl: targetCheckoutUrl, plusCheckoutCountry: result.country || 'DE', plusCheckoutCurrency: result.currency || 'EUR', plusCheckoutSource: '', diff --git a/content/paypal-flow.js b/content/paypal-flow.js index 689811e..9fd8782 100644 --- a/content/paypal-flow.js +++ b/content/paypal-flow.js @@ -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, diff --git a/content/plus-checkout.js b/content/plus-checkout.js index b3f7f2a..7ca4353 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -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 || '', diff --git a/data/step-definitions.js b/data/step-definitions.js index 27b0a0a..e7b2a8d 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -3,6 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() { const DEFAULT_ACTIVE_FLOW_ID = 'openai'; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; + const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth'; @@ -32,6 +33,7 @@ { id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' }, { id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' }, ]; + const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6); const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, @@ -163,6 +165,53 @@ ]; } + function createHostedCheckoutAuthTail(startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) { + const id = Number(startId) || 7; + const order = Number(startOrder) || id * 10; + const commonStart = [ + { id, order, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, + { id: id + 1, order: order + 10, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, + ]; + + if (signupMethod === SIGNUP_METHOD_PHONE) { + if (isPhoneSignupReloginAfterBindEmailEnabled(options)) { + return [ + ...commonStart, + { id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' }, + { id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' }, + { id: id + 4, order: order + 40, key: 'relogin-bound-email', title: '绑定邮箱后刷新 OAuth 并登录(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, + { id: id + 5, order: order + 50, key: 'fetch-bound-email-login-code', title: '获取登录验证码(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, + { id: id + 6, order: order + 60, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, + { id: id + 7, order: order + 70, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, + ]; + } + return [ + ...commonStart, + { id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' }, + { id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' }, + { id: id + 4, order: order + 40, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, + { id: id + 5, order: order + 50, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, + ]; + } + + return [ + ...commonStart, + { id: id + 2, order: order + 20, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, + { id: id + 3, order: order + 30, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, + ]; + } + + function createHostedCheckoutSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) { + const sessionTailFactory = resolvePlusSessionImportTail(options, signupMethod); + const tailSteps = sessionTailFactory + ? sessionTailFactory(startId, startOrder) + : createHostedCheckoutAuthTail(startId, startOrder, signupMethod, options); + return [ + ...prefixSteps, + ...tailSteps, + ]; + } + const NORMAL_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL); const NORMAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE); const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); @@ -183,6 +232,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, 7, 70, SIGNUP_METHOD_EMAIL); + const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps( + PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, + 7, + 70, + 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, + 7, + 70, + 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, 7, 70, SIGNUP_METHOD_PHONE); + const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, 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, @@ -313,6 +379,9 @@ function normalizePlusPaymentMethod(value = '') { const normalized = String(value || '').trim().toLowerCase(); + if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') { + return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED; + } if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_PAYMENT_METHOD_GPC_HELPER; } @@ -352,6 +421,20 @@ } const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod); const plusAccountAccessStrategy = normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy); + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + if (signupMethod === SIGNUP_METHOD_PHONE) { + return reloginAfterBindEmail + ? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS + : PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS; + } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { + return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS; + } + if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) { + return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS; + } + return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS; + } if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { if (signupMethod === SIGNUP_METHOD_PHONE) { return reloginAfterBindEmail @@ -433,6 +516,11 @@ ...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, ...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS, + ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, ...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, ...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS, @@ -639,6 +727,11 @@ PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS, PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, + PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS, + PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS, + PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS, + PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS, + PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS, PLUS_GOPAY_STEP_DEFINITIONS, PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS, PLUS_GOPAY_PHONE_STEP_DEFINITIONS, diff --git a/gopay-utils.js b/gopay-utils.js index 0c25a0a..1f110b4 100644 --- a/gopay-utils.js +++ b/gopay-utils.js @@ -2,6 +2,7 @@ root.GoPayUtils = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() { const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; + const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; @@ -11,6 +12,9 @@ function normalizePlusPaymentMethod(value = '') { const normalized = String(value || '').trim().toLowerCase(); + if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') { + return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED; + } if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) { return PLUS_PAYMENT_METHOD_GPC_HELPER; } @@ -416,6 +420,7 @@ PLUS_PAYMENT_METHOD_GPC_HELPER, PLUS_PAYMENT_METHOD_GOPAY, PLUS_PAYMENT_METHOD_PAYPAL, + PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, buildGpcCardBalanceUrl, buildGpcApiKeyBalanceUrl, buildGpcApiKeyHeaders, diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js index d5f430f..4c05fa0 100644 --- a/shared/flow-capabilities.js +++ b/shared/flow-capabilities.js @@ -378,24 +378,31 @@ const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy( options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy ); + const basePlusAccountAccessStrategies = [ + PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH, + PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION, + PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION, + ]; const availablePlusAccountAccessStrategies = activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode) && Boolean(runtimeLocks.plusModeEnabled) && effectiveSignupMethod === SIGNUP_METHOD_EMAIL - ? ( - Array.isArray(targetState.supportedPlusAccountAccessStrategies) - && targetState.supportedPlusAccountAccessStrategies.length > 0 - ? targetState.supportedPlusAccountAccessStrategies.slice() - : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH] - ) + ? (runtimeLocks.accountContribution + ? [PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION] + : basePlusAccountAccessStrategies) : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]; - const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy) + const effectivePlusAccountAccessStrategy = runtimeLocks.accountContribution + && runtimeLocks.plusModeEnabled + && effectiveSignupMethod === SIGNUP_METHOD_EMAIL + ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION + : availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy) ? requestedPlusAccountAccessStrategy : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH; const canEditPlusAccountAccessStrategy = activeFlowId === 'openai' && Boolean(flowState.supportsPlusMode) && Boolean(runtimeLocks.plusModeEnabled) && effectiveSignupMethod === SIGNUP_METHOD_EMAIL + && !runtimeLocks.accountContribution && availablePlusAccountAccessStrategies.length > 1; const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function' && isRegisteredFlowId(activeFlowId) diff --git a/shared/settings-schema.js b/shared/settings-schema.js index 2f39824..8b6f7ad 100644 --- a/shared/settings-schema.js +++ b/shared/settings-schema.js @@ -105,8 +105,11 @@ }, plus: { plusModeEnabled: false, - plusPaymentMethod: 'paypal', + plusPaymentMethod: 'paypal-hosted', plusAccountAccessStrategy: 'oauth', + hostedCheckoutVerificationUrl: '', + hostedCheckoutPhoneNumber: '', + plusHostedCheckoutOauthDelaySeconds: 3, }, autoRun: { stepExecutionRange: { @@ -317,6 +320,24 @@ ?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy ?? defaults.flows.openai.plus.plusAccountAccessStrategy ), + hostedCheckoutVerificationUrl: String( + input?.hostedCheckoutVerificationUrl + ?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl + ?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl + ).trim(), + hostedCheckoutPhoneNumber: String( + input?.hostedCheckoutPhoneNumber + ?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber + ?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber + ).trim(), + plusHostedCheckoutOauthDelaySeconds: (() => { + const numeric = Number( + input?.plusHostedCheckoutOauthDelaySeconds + ?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds + ?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds + ); + return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds))); + })(), }, autoRun: { stepExecutionRange: normalizeStepExecutionRangeEntry( @@ -456,6 +477,9 @@ next.plusModeEnabled = openaiState.plus.plusModeEnabled; next.plusPaymentMethod = openaiState.plus.plusPaymentMethod; next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy; + next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl; + next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber; + next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds; next.mailProvider = normalizedState.services.email.provider; next.ipProxyEnabled = normalizedState.services.proxy.enabled; next.ipProxyService = normalizedState.services.proxy.provider; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 87d721b..1a92415 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -327,6 +327,7 @@ Plus 支付