diff --git a/background.js b/background.js index 0343a99..82021f6 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': @@ -10626,6 +10711,11 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'fetch-signup-code', 'wait-registration-success', 'plus-checkout-create', + 'paypal-hosted-openai-checkout', + 'paypal-hosted-email', + 'paypal-hosted-card', + 'paypal-hosted-create-account', + 'paypal-hosted-review', 'plus-checkout-billing', 'paypal-approve', 'plus-checkout-return', @@ -11725,6 +11815,11 @@ const AUTO_RUN_NODE_DELAYS = Object.freeze({ 'fill-profile': 0, 'wait-registration-success': 3000, 'plus-checkout-create': 3000, + 'paypal-hosted-openai-checkout': 2000, + 'paypal-hosted-email': 2000, + 'paypal-hosted-card': 2000, + 'paypal-hosted-create-account': 2000, + 'paypal-hosted-review': 2000, 'plus-checkout-billing': 2000, 'gopay-subscription-confirm': 2000, 'paypal-approve': 2000, @@ -13453,13 +13548,18 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c createAutomationTab, ensureContentScriptReadyOnTabUntilStopped, fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getTabId, + getState, + isTabAlive, markCurrentRegistrationAccountUsed, + queryTabsInAutomationWindow, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, throwIfStopped, waitForTabCompleteUntilStopped, + waitForTabUrlMatchUntilStopped, }); const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({ addLog, @@ -13735,6 +13835,11 @@ const stepExecutorsByKey = { 'fill-profile': (state) => step5Executor.executeStep5(state), 'wait-registration-success': (state) => step6Executor.executeStep6(state), 'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state), + 'paypal-hosted-openai-checkout': (state) => plusCheckoutCreateExecutor.executePayPalHostedOpenAiCheckout(state), + 'paypal-hosted-email': (state) => plusCheckoutCreateExecutor.executePayPalHostedEmail(state), + 'paypal-hosted-card': (state) => plusCheckoutCreateExecutor.executePayPalHostedCard(state), + 'paypal-hosted-create-account': (state) => plusCheckoutCreateExecutor.executePayPalHostedCreateAccount(state), + 'paypal-hosted-review': (state) => plusCheckoutCreateExecutor.executePayPalHostedReview(state), 'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state), 'gopay-subscription-confirm': (state) => goPayManualConfirmExecutor.executeGoPayManualConfirm(state), 'paypal-approve': (state) => normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 3e5fb30..d29b99f 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -2,14 +2,43 @@ 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'; + 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_CREATE_ACCOUNT = 'create_account'; + const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent'; + const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval'; + const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown'; + const PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT = 'paypal-hosted-openai-checkout'; + const PAYPAL_HOSTED_STEP_EMAIL = 'paypal-hosted-email'; + const PAYPAL_HOSTED_STEP_CARD = 'paypal-hosted-card'; + const PAYPAL_HOSTED_STEP_CREATE_ACCOUNT = 'paypal-hosted-create-account'; + const PAYPAL_HOSTED_STEP_REVIEW = 'paypal-hosted-review'; + const PAYPAL_HOSTED_STEP_META = Object.freeze({ + [PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT]: { step: 6, label: '创建 PayPal 无卡直绑 Checkout' }, + [PAYPAL_HOSTED_STEP_EMAIL]: { step: 7, label: '无卡直绑 PayPal 邮箱页' }, + [PAYPAL_HOSTED_STEP_CARD]: { step: 8, label: '无卡直绑 PayPal 资料页' }, + [PAYPAL_HOSTED_STEP_CREATE_ACCOUNT]: { step: 9, label: '无卡直绑 PayPal 创建确认页' }, + [PAYPAL_HOSTED_STEP_REVIEW]: { step: 10, label: '无卡直绑 PayPal 授权复核页' }, + }); function createPlusCheckoutCreateExecutor(deps = {}) { const { @@ -19,11 +48,16 @@ createAutomationTab = null, ensureContentScriptReadyOnTabUntilStopped, fetch: fetchImpl = null, + getTabId = null, + getState = null, + isTabAlive = null, + queryTabsInAutomationWindow = null, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, + waitForTabUrlMatchUntilStopped = null, throwIfStopped = () => {}, } = deps; @@ -35,12 +69,24 @@ }); } + function addHostedStepLog(stepKey, message, level = 'info', options = {}) { + const meta = PAYPAL_HOSTED_STEP_META[stepKey] || {}; + return rawAddLog(message, level, { + step: meta.step || 6, + stepKey, + ...(options && typeof options === 'object' ? options : {}), + }); + } + function normalizePlusPaymentMethod(value = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) { 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 +98,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 +109,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 +129,872 @@ 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 || '')); + } + + function isHostedCheckoutRuntimeUrl(url = '') { + return isHostedOpenAiCheckoutUrl(url) || isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url); + } + + function getHostedStepNumber(stepKey = '') { + return PAYPAL_HOSTED_STEP_META[stepKey]?.step || 6; + } + + function normalizeHostedPhoneForPayload(phone = '') { + const digits = String(phone || '').replace(/\D/g, ''); + if (!digits) { + return HOSTED_CHECKOUT_DEFAULT_PHONE; + } + if (digits.length > 10 && digits.startsWith('1')) { + return digits.slice(-10); + } + return digits.length > 10 ? digits.slice(-10) : digits; + } + + function getHostedProfileFromState(state = {}) { + const profile = state?.plusHostedCheckoutGuestProfile || state?.hostedCheckoutGuestProfile || null; + return profile && typeof profile === 'object' && !Array.isArray(profile) ? profile : null; + } + + async function getLatestHostedState(state = {}) { + const latestState = typeof getState === 'function' ? await getState().catch(() => ({})) : {}; + return { + ...(latestState && typeof latestState === 'object' ? latestState : {}), + ...(state && typeof state === 'object' ? state : {}), + }; + } + + async function ensureHostedGuestProfile(state = {}) { + const mergedState = await getLatestHostedState(state); + const existingProfile = getHostedProfileFromState(mergedState) || {}; + const config = await getHostedCheckoutRuntimeConfig(mergedState); + const address = existingProfile.address && typeof existingProfile.address === 'object' + ? existingProfile.address + : await fetchHostedCheckoutAddress(); + const generatedProfile = buildHostedGuestProfile(address, { + phone: normalizeHostedPhoneForPayload(config.phone), + }); + const nextProfile = { + ...generatedProfile, + ...existingProfile, + address, + phone: normalizeHostedPhoneForPayload(config.phone || existingProfile.phone), + }; + await setState({ + plusHostedCheckoutGuestProfile: nextProfile, + plusHostedCheckoutPhoneDigits: nextProfile.phone, + }); + return { + profile: nextProfile, + config, + }; + } + + async function getTabById(tabId) { + const normalizedTabId = Number(tabId) || 0; + if (!normalizedTabId || !chrome?.tabs?.get) { + return null; + } + return chrome.tabs.get(normalizedTabId).catch(() => null); + } + + async function registerHostedCheckoutTab(tabId, url = '') { + if (typeof registerTab !== 'function' || !Number.isInteger(Number(tabId))) { + return; + } + await registerTab(isPayPalUrl(url) ? PAYPAL_SOURCE : PLUS_CHECKOUT_SOURCE, Number(tabId)); + } + + async function findOpenHostedCheckoutTabId() { + const queryTabs = typeof queryTabsInAutomationWindow === 'function' + ? queryTabsInAutomationWindow + : (chrome?.tabs?.query ? (queryInfo) => chrome.tabs.query(queryInfo) : null); + if (typeof queryTabs !== 'function') { + return 0; + } + const tabs = await queryTabs({}).catch(() => []); + const candidates = (Array.isArray(tabs) ? tabs : []) + .filter((tab) => Number.isInteger(tab?.id) && isHostedCheckoutRuntimeUrl(tab.url || '')); + if (!candidates.length) { + return 0; + } + const match = candidates.find((tab) => tab.active && tab.currentWindow) + || candidates.find((tab) => tab.active) + || candidates[0]; + if (match?.id && chrome?.tabs?.update) { + await chrome.tabs.update(match.id, { active: true }).catch(() => {}); + } + await registerHostedCheckoutTab(match.id, match.url || ''); + return match?.id || 0; + } + + async function resolveHostedCheckoutTabId(state = {}, stepKey = '') { + const storedTabId = Number(state?.plusCheckoutTabId) || 0; + const storedTab = await getTabById(storedTabId); + if (storedTab?.id && isHostedCheckoutRuntimeUrl(storedTab.url || '')) { + await registerHostedCheckoutTab(storedTab.id, storedTab.url || ''); + return storedTab.id; + } + + if (typeof getTabId === 'function') { + const paypalTabId = await Promise.resolve(getTabId(PAYPAL_SOURCE)).catch(() => 0); + const paypalAlive = typeof isTabAlive !== 'function' + ? Boolean(paypalTabId) + : await Promise.resolve(isTabAlive(PAYPAL_SOURCE)).catch(() => false); + if (paypalTabId && paypalAlive) { + return paypalTabId; + } + const checkoutTabId = await Promise.resolve(getTabId(PLUS_CHECKOUT_SOURCE)).catch(() => 0); + const checkoutAlive = typeof isTabAlive !== 'function' + ? Boolean(checkoutTabId) + : await Promise.resolve(isTabAlive(PLUS_CHECKOUT_SOURCE)).catch(() => false); + if (checkoutTabId && checkoutAlive) { + return checkoutTabId; + } + } + + const discoveredTabId = await findOpenHostedCheckoutTabId(); + if (discoveredTabId) { + await addHostedStepLog(stepKey, `步骤 ${getHostedStepNumber(stepKey)}:已从当前浏览器标签中发现 PayPal 无卡直绑页面,正在接管继续执行。`, 'info'); + return discoveredTabId; + } + + throw new Error(`步骤 ${getHostedStepNumber(stepKey)}:未找到 PayPal 无卡直绑标签页,请先完成创建 checkout 节点。`); + } + + async function getHostedCurrentUrl(tabId) { + const tab = await getTabById(tabId); + return String(tab?.url || '').trim(); + } + + async function updateHostedCheckoutTabState(tabId, payload = {}) { + const currentUrl = await getHostedCurrentUrl(tabId); + await setState({ + plusCheckoutTabId: tabId, + plusCheckoutUrl: currentUrl, + ...(payload && typeof payload === 'object' ? payload : {}), + }); + return currentUrl; + } + + async function completeHostedStep(stepKey, tabId, payload = {}) { + const currentUrl = await updateHostedCheckoutTabState(tabId, payload); + await completeNodeFromBackground(stepKey, { + plusCheckoutUrl: currentUrl, + ...(payload && typeof payload === 'object' ? payload : {}), + }); + } + + async function completeHostedStepIfSuccessful(stepKey, tabId, state = {}, options = {}) { + const currentUrl = await getHostedCurrentUrl(tabId); + if (!isHostedCheckoutSuccessUrl(currentUrl)) { + return false; + } + const config = await getHostedCheckoutRuntimeConfig(state); + const shouldWait = Boolean(options.waitBeforeComplete); + if (shouldWait && config.oauthDelaySeconds > 0) { + await addHostedStepLog(stepKey, `步骤 ${getHostedStepNumber(stepKey)}:支付成功后等待 ${config.oauthDelaySeconds} 秒,再继续账号接入。`, 'info'); + await sleepWithStop(config.oauthDelaySeconds * 1000); + } + await completeHostedStep(stepKey, tabId, { + plusReturnUrl: currentUrl, + plusHostedCheckoutCompleted: true, + plusHostedCheckoutOauthDelaySeconds: config.oauthDelaySeconds, + }); + return true; + } + + 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( + latestState?.hostedCheckoutVerificationUrl + || state?.hostedCheckoutVerificationUrl + || '' + ).trim(), + phone: String( + latestState?.hostedCheckoutPhoneNumber + || state?.hostedCheckoutPhoneNumber + || HOSTED_CHECKOUT_DEFAULT_PHONE + ).trim(), + oauthDelaySeconds: normalizeHostedCheckoutDelaySeconds( + latestState?.plusHostedCheckoutOauthDelaySeconds + ?? state?.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('未配置 OpenAI Checkout 验证码接口。'); + } + const fetcher = typeof fetchImpl === 'function' + ? fetchImpl + : (typeof fetch === 'function' ? fetch.bind(globalThis) : null); + if (typeof fetcher !== 'function') { + throw new Error('当前运行环境不支持 fetch,无法获取 OpenAI Checkout 验证码。'); + } + 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:已获取 OpenAI Checkout 验证码(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS})。`, 'info'); + return code; + } catch (error) { + lastError = error; + await addLog(`步骤 6:OpenAI Checkout 验证码暂不可用(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS}):${error?.message || error}`, 'warn'); + if (attempt < HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS) { + await sleepWithStop(HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS); + } + } + } + throw lastError || new Error('OpenAI Checkout 验证码轮询失败。'); + } + + 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 || {}; + } + + function getHostedStageOrder(stage = '') { + switch (stage) { + case PAYPAL_HOSTED_STAGE_LOGIN: + return 1; + case PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT: + return 2; + case PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT: + return 3; + case PAYPAL_HOSTED_STAGE_REVIEW: + return 4; + case PAYPAL_HOSTED_STAGE_OUTSIDE: + return 5; + default: + return 0; + } + } + + function isHostedStageAtOrAfter(stage = '', expectedStage = '') { + const currentOrder = getHostedStageOrder(stage); + const expectedOrder = getHostedStageOrder(expectedStage); + return currentOrder > 0 && expectedOrder > 0 && currentOrder >= expectedOrder; + } + + async function waitForHostedPayPalStage(tabId, predicate, options = {}) { + const timeoutMs = Math.max(1000, Number(options.timeoutMs) || HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS); + const intervalMs = Math.max(100, Number(options.intervalMs) || 500); + const label = String(options.label || 'PayPal 无卡直绑页面').trim(); + const deadline = Date.now() + timeoutMs; + let lastStage = ''; + while (Date.now() < deadline) { + throwIfStopped(); + const currentUrl = await getHostedCurrentUrl(tabId); + if (isHostedCheckoutSuccessUrl(currentUrl)) { + return { + successUrl: currentUrl, + hostedStage: PAYPAL_HOSTED_STAGE_OUTSIDE, + }; + } + if (!isPayPalUrl(currentUrl)) { + await sleepWithStop(intervalMs); + continue; + } + try { + const pageState = await getHostedPayPalState(tabId); + lastStage = pageState?.hostedStage || lastStage; + if (predicate(pageState)) { + return pageState; + } + } catch (error) { + lastStage = error?.message || lastStage; + } + await sleepWithStop(intervalMs); + } + throw new Error(`${label}等待超时${lastStage ? `(最后状态:${lastStage})` : ''}。`); + } + + async function waitForHostedUrlAfterAction(tabId, matcher, options = {}) { + const timeoutMs = Math.max(1000, Number(options.timeoutMs) || HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS); + const intervalMs = Math.max(100, Number(options.intervalMs) || 500); + const label = String(options.label || 'PayPal 无卡直绑跳转').trim(); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + throwIfStopped(); + const currentTab = await getTabById(tabId); + const currentUrl = String(currentTab?.url || '').trim(); + if (matcher(currentUrl, currentTab)) { + await waitForTabCompleteUntilStopped(tabId).catch(() => {}); + return currentUrl; + } + await sleepWithStop(intervalMs); + } + throw new Error(`${label}等待超时。`); + } + + async function runHostedPayPalStepAndWaitForStageChange(tabId, payload = {}, previousStage = '', options = {}) { + const normalizedPreviousStage = String(previousStage || payload.expectedStage || '').trim(); + const label = String(options.label || 'PayPal 无卡直绑页面跳转').trim(); + const predicate = typeof options.predicate === 'function' + ? options.predicate + : (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== normalizedPreviousStage; + const stageChangePromise = waitForHostedPayPalStage(tabId, predicate, { + label, + timeoutMs: options.timeoutMs || HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS, + intervalMs: options.intervalMs || 500, + }).then( + (nextState) => ({ type: 'stage-change', nextState }), + (error) => ({ type: 'stage-error', error }) + ); + const actionPromise = runHostedPayPalStep(tabId, payload).then( + (result) => ({ type: 'action', result }), + (error) => ({ type: 'action-error', error }) + ); + + const first = await Promise.race([actionPromise, stageChangePromise]); + if (first.type === 'stage-change') { + return { + result: null, + nextState: first.nextState, + completedByStageChange: true, + }; + } + if (first.type === 'action-error') { + throw first.error; + } + if (first.type === 'stage-error') { + throw first.error; + } + + const stageOutcome = await stageChangePromise; + if (stageOutcome.type === 'stage-change') { + return { + result: first.result, + nextState: stageOutcome.nextState, + completedByStageChange: false, + }; + } + throw stageOutcome.error; + } + + function resolveCheckoutTargetUrl(result = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) { + if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) { + return String( + 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 无卡直绑链接已创建,正在打开并提交 OpenAI 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(); + let completedUrl = 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({ + plusCheckoutTabId: tabId, + plusCheckoutUrl: completedUrl, + plusCheckoutCountry: result.country || 'US', + plusCheckoutCurrency: result.currency || 'USD', + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + plusReturnUrl: isAlreadySuccessful ? completedUrl : '', + plusHostedCheckoutCompleted: isAlreadySuccessful, + }); + + await addLog(`步骤 6:PayPal 无卡直绑已提交 OpenAI Checkout(${result.country || 'US'} ${result.currency || 'USD'}),准备进入 PayPal 邮箱页。`, 'info'); + + await completeNodeFromBackground('plus-checkout-create', { + plusCheckoutCountry: result.country || 'US', + plusCheckoutCurrency: result.currency || 'USD', + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + plusCheckoutUrl: completedUrl, + plusReturnUrl: isAlreadySuccessful ? completedUrl : '', + plusHostedCheckoutCompleted: isAlreadySuccessful, + }); + } + + async function executePayPalHostedOpenAiCheckout(state = {}) { + const stepKey = PAYPAL_HOSTED_STEP_OPENAI_CHECKOUT; + const stepNumber = getHostedStepNumber(stepKey); + const tabId = await resolveHostedCheckoutTabId(state, stepKey); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + + let currentUrl = await getHostedCurrentUrl(tabId); + if (isPayPalUrl(currentUrl)) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前已在 PayPal 页面,OpenAI Checkout 节点直接完成。`, 'info'); + await completeHostedStep(stepKey, tabId, { + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + }); + return; + } + if (!isHostedOpenAiCheckoutUrl(currentUrl)) { + currentUrl = await waitForHostedUrlAfterAction( + tabId, + (url) => isHostedOpenAiCheckoutUrl(url) || isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 OpenAI hosted checkout 页面` } + ); + } + if (isHostedCheckoutSuccessUrl(currentUrl)) { + await completeHostedStep(stepKey, tabId, { + plusReturnUrl: currentUrl, + plusHostedCheckoutCompleted: true, + }); + return; + } + if (isPayPalUrl(currentUrl)) { + await completeHostedStep(stepKey, tabId, { + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + }); + return; + } + + const { profile, config } = await ensureHostedGuestProfile(state); + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在选择 PayPal 并提交 OpenAI hosted checkout(电话使用本地号码 ${profile.phone})。`, 'info'); + const transitionUrl = await runHostedOpenAiCheckout(tabId, profile, config); + const completedUrl = String(transitionUrl || await getHostedCurrentUrl(tabId) || '').trim(); + await completeHostedStep(stepKey, tabId, { + plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, + plusCheckoutUrl: completedUrl, + plusReturnUrl: isHostedCheckoutSuccessUrl(completedUrl) ? completedUrl : '', + plusHostedCheckoutCompleted: isHostedCheckoutSuccessUrl(completedUrl), + }); + } + + async function executePayPalHostedEmail(state = {}) { + const stepKey = PAYPAL_HOSTED_STEP_EMAIL; + const stepNumber = getHostedStepNumber(stepKey); + const tabId = await resolveHostedCheckoutTabId(state, stepKey); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + const { profile } = await ensureHostedGuestProfile(state); + await waitForHostedUrlAfterAction( + tabId, + (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 PayPal 邮箱页` } + ); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + + const pageState = await getHostedPayPalState(tabId); + if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) + && pageState.hostedStage !== PAYPAL_HOSTED_STAGE_LOGIN) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),邮箱节点直接完成。`, 'info'); + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: pageState.hostedStage, + }); + return; + } + if (pageState.hostedStage !== PAYPAL_HOSTED_STAGE_LOGIN) { + throw new Error(`步骤 ${stepNumber}:当前不是 PayPal 邮箱页(当前状态:${pageState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN})。`); + } + + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在填写 PayPal 无卡直绑邮箱。`, 'info'); + const { nextState, completedByStageChange } = await runHostedPayPalStepAndWaitForStageChange(tabId, { + expectedStage: PAYPAL_HOSTED_STAGE_LOGIN, + email: profile.email, + }, PAYPAL_HOSTED_STAGE_LOGIN, { label: `步骤 ${stepNumber}:等待 PayPal 邮箱页跳转` }); + if (completedByStageChange) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:已检测到 PayPal 进入后续页面(${nextState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN}),邮箱节点直接完成。`, 'info'); + } + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: nextState.hostedStage || '', + }); + } + + async function executePayPalHostedCard(state = {}) { + const stepKey = PAYPAL_HOSTED_STEP_CARD; + const stepNumber = getHostedStepNumber(stepKey); + const tabId = await resolveHostedCheckoutTabId(state, stepKey); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + await waitForHostedUrlAfterAction( + tabId, + (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 PayPal 资料页` } + ); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + + const pageState = await getHostedPayPalState(tabId); + if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) + && pageState.hostedStage !== PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),资料节点直接完成。`, 'info'); + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: pageState.hostedStage, + }); + return; + } + if (pageState.hostedStage !== PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) { + throw new Error(`步骤 ${stepNumber}:当前不是 PayPal 资料页(当前状态:${pageState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN})。`); + } + + const { profile } = await ensureHostedGuestProfile(state); + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在填写 PayPal 无卡直绑资料,提交前会复查电话是否为 ${profile.phone}。`, 'info'); + const cardResult = await runHostedPayPalStep(tabId, { + ...profile, + expectedStage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT, + phone: profile.phone, + }); + if (cardResult?.phoneMatched) { + await addHostedStepLog( + stepKey, + `步骤 ${stepNumber}:PayPal 页面电话复查通过(配置 ${cardResult.payloadPhoneDigits},页面 ${cardResult.renderedPhoneDigits})。`, + 'info' + ); + } + const nextState = await waitForHostedPayPalStage( + tabId, + (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT, + { label: `步骤 ${stepNumber}:等待 PayPal 资料页跳转` } + ); + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: nextState.hostedStage || '', + }); + } + + async function executePayPalHostedCreateAccount(state = {}) { + const stepKey = PAYPAL_HOSTED_STEP_CREATE_ACCOUNT; + const stepNumber = getHostedStepNumber(stepKey); + const tabId = await resolveHostedCheckoutTabId(state, stepKey); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + await waitForHostedUrlAfterAction( + tabId, + (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 PayPal 创建确认页` } + ); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state)) { + return; + } + + const pageState = await getHostedPayPalState(tabId); + if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_REVIEW) + && pageState.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) { + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),创建确认节点直接完成。`, 'info'); + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: pageState.hostedStage, + }); + return; + } + if (pageState.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) { + throw new Error(`步骤 ${stepNumber}:当前不是 PayPal 创建确认页(当前状态:${pageState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN})。`); + } + + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在确认创建 PayPal 账号。`, 'info'); + await runHostedPayPalStep(tabId, { + expectedStage: PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT, + }); + const nextState = await waitForHostedPayPalStage( + tabId, + (stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT, + { label: `步骤 ${stepNumber}:等待 PayPal 创建确认页跳转` } + ); + await completeHostedStep(stepKey, tabId, { + plusHostedCheckoutLastStage: nextState.hostedStage || '', + }); + } + + async function executePayPalHostedReview(state = {}) { + const stepKey = PAYPAL_HOSTED_STEP_REVIEW; + const stepNumber = getHostedStepNumber(stepKey); + const tabId = await resolveHostedCheckoutTabId(state, stepKey); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state, { waitBeforeComplete: true })) { + return; + } + await waitForHostedUrlAfterAction( + tabId, + (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 PayPal 授权复核页` } + ); + if (await completeHostedStepIfSuccessful(stepKey, tabId, state, { waitBeforeComplete: true })) { + return; + } + + const pageState = await getHostedPayPalState(tabId); + if (pageState.hostedStage !== PAYPAL_HOSTED_STAGE_REVIEW) { + throw new Error(`步骤 ${stepNumber}:当前不是 PayPal 授权复核页(当前状态:${pageState.hostedStage || PAYPAL_HOSTED_STAGE_UNKNOWN})。`); + } + + await addHostedStepLog(stepKey, `步骤 ${stepNumber}:正在确认 PayPal 授权复核页。`, 'info'); + await runHostedPayPalStep(tabId, { + expectedStage: PAYPAL_HOSTED_STAGE_REVIEW, + }); + await waitForHostedUrlAfterAction( + tabId, + (url) => isHostedCheckoutSuccessUrl(url), + { label: `步骤 ${stepNumber}:等待 PayPal 回到 ChatGPT 支付成功页`, timeoutMs: HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS } + ); + if (!await completeHostedStepIfSuccessful(stepKey, tabId, state, { waitBeforeComplete: true })) { + throw new Error(`步骤 ${stepNumber}:PayPal 授权后未检测到 ChatGPT 支付成功页。`); + } + } + function normalizeHelperCountryCode(countryCode = '86') { const digits = String(countryCode || '').replace(/\D/g, ''); return digits || '86'; @@ -493,12 +1411,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 +1433,7 @@ await setState({ plusCheckoutTabId: tabId, - plusCheckoutUrl: result.checkoutUrl, + plusCheckoutUrl: targetCheckoutUrl, plusCheckoutCountry: result.country || 'DE', plusCheckoutCurrency: result.currency || 'EUR', plusCheckoutSource: '', @@ -525,6 +1449,11 @@ return { executePlusCheckoutCreate, + executePayPalHostedOpenAiCheckout, + executePayPalHostedEmail, + executePayPalHostedCard, + executePayPalHostedCreateAccount, + executePayPalHostedReview, }; } diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 6255b8d..6a4d813 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -5,8 +5,8 @@ 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_FRAME_READY_DELAY_MS = 500; - const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3; - const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000; + const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 5; + const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 10000; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; @@ -1883,7 +1883,7 @@ await addLog( attempt === 1 ? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...' - : `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新提交账单地址...`, + : `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新检测订阅按钮...`, attempt === 1 ? 'info' : 'warn' ); await sleepWithStop(3000); @@ -1907,17 +1907,27 @@ 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); if (redirectedToPayment) { break; } - lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`; - await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn'); + lastSubmitError = subscribeClicked + ? `点击订阅后 ${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) { - throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`); + throw new Error(`步骤 7:多次检测订阅按钮后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`); } await completeNodeFromBackground('plus-checkout-billing', { diff --git a/content/paypal-flow.js b/content/paypal-flow.js index 689811e..959245d 100644 --- a/content/paypal-flow.js +++ b/content/paypal-flow.js @@ -3,6 +3,20 @@ 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_CREATE_ACCOUNT = 'create_account'; +const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent'; +const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval'; +const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown'; +const PAYPAL_HOSTED_STEP_KEYS = { + [PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email', + [PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card', + [PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account', + [PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review', +}; if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') { document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1'); @@ -13,6 +27,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 +63,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 +243,429 @@ function findApproveButton() { ]); } +function getPayPalPathname() { + return String(location?.pathname || '').trim(); +} + +function isHostedLoginPage() { + return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email')); +} + +function isHostedGuestCheckoutPage() { + if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) { + return true; + } + const pageText = normalizeText(document.body?.innerText || document.body?.textContent || ''); + if (/create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText) + && findHostedCreateAccountButton()) { + return false; + } + return /\/checkoutweb\//i.test(getPayPalPathname()) + && Boolean(document.getElementById('phone') || document.getElementById('email')); +} + +function isHostedReviewPage() { + return /\/webapps\/hermes/i.test(getPayPalPathname()); +} + +function findHostedCreateAccountButton() { + return document.getElementById('createAccount') + || document.getElementById('createAccountButton') + || document.querySelector('button[data-testid="createAccountButton"]') + || document.querySelector('button[data-testid="create-account-button"]') + || findClickableByText([ + /agree\s*(?:&|and)?\s*create\s*(?:paypal\s*)?account/i, + /create\s*(?:paypal\s*)?account/i, + /同意.*创建|创建.*账户|创建.*账号/i, + ]); +} + +function isHostedCreateAccountPage() { + if (isHostedLoginPage()) { + return false; + } + if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) { + return false; + } + const button = findHostedCreateAccountButton(); + if (!button || !isVisibleElement(button) || !isEnabledControl(button)) { + return false; + } + const pageText = normalizeText(document.body?.innerText || document.body?.textContent || ''); + return /create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText) + || /create/i.test(getActionText(button)); +} + +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 (isHostedGuestCheckoutPage()) { + return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT; + } + if (isHostedReviewPage() && findHostedReviewConsentButton()) { + return PAYPAL_HOSTED_STAGE_REVIEW; + } + if (isHostedCreateAccountPage()) { + return PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT; + } + 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, + ]); +} + +function getHostedStepKey(stage = '', fallback = 'plus-checkout-create') { + return PAYPAL_HOSTED_STEP_KEYS[stage] || fallback; +} + +async function clickHostedSubmitButton(options = {}) { + const stepKey = String(options.stepKey || getHostedStepKey(options.stage)).trim(); + const label = String(options.label || 'hosted-paypal-submit').trim(); + const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || 3)); + let lastButtonText = ''; + let lastDisabled = false; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const button = await waitUntil(() => { + const candidate = findHostedSubmitButton(); + return candidate && isVisibleElement(candidate) ? candidate : null; + }, { + intervalMs: 500, + timeoutMs: 15000, + timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。', + }); + lastButtonText = getActionText(button); + lastDisabled = !isEnabledControl(button); + if (lastDisabled) { + if (attempt >= maxAttempts) { + throw new Error('PayPal hosted checkout 继续/提交按钮长时间不可用。'); + } + await sleep(1000); + continue; + } + + await performPayPalOperationWithDelay({ stepKey, kind: 'click', label }, async () => { + simulateClick(button); + }); + await sleep(1000); + return { + clicked: true, + buttonText: lastButtonText, + attempt, + }; + } + return { + clicked: false, + buttonText: lastButtonText, + disabled: lastDisabled, + }; +} + +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); + }); + return { + clicked: true, + buttonText, + }; +} + +function normalizeHostedPhoneDigits(value = '') { + return String(value || '').replace(/\D/g, ''); +} + +function verifyHostedPhoneBeforeSubmit(expectedPhone = '') { + const phoneInput = document.getElementById('phone'); + if (!phoneInput || !isVisibleElement(phoneInput)) { + throw new Error('PayPal hosted checkout 未找到电话输入框。'); + } + const expectedDigits = normalizeHostedPhoneDigits(expectedPhone || PAYPAL_HOSTED_DEFAULT_PHONE); + const renderedDigits = normalizeHostedPhoneDigits(phoneInput.value || ''); + if (!expectedDigits) { + throw new Error('PayPal hosted checkout 电话配置为空。'); + } + const comparableRenderedDigits = renderedDigits.length > expectedDigits.length + ? renderedDigits.slice(-expectedDigits.length) + : renderedDigits; + if (comparableRenderedDigits !== expectedDigits) { + throw new Error(`PayPal hosted checkout 电话不一致:配置 ${expectedDigits},页面 ${renderedDigits || '(空)'}。`); + } + return { + payloadPhoneDigits: expectedDigits, + renderedPhoneDigits: renderedDigits, + phoneMatched: true, + }; +} + +async function clickHostedCreateAccount(payload = {}) { + await waitForDocumentComplete(); + const button = await waitUntil(() => { + const candidate = findHostedCreateAccountButton(); + return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null; + }, { + intervalMs: 500, + timeoutMs: 30000, + timeoutMessage: 'PayPal hosted checkout 未找到创建账号确认按钮。', + }); + await performPayPalOperationWithDelay({ + stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT), + kind: 'click', + label: 'hosted-paypal-create-account', + }, async () => { + simulateClick(button); + }); + return { + stage: PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT, + clicked: true, + submitted: true, + buttonText: getActionText(button), + }; +} + +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 clickHostedEmailNextButton(); + return { + stage: PAYPAL_HOSTED_STAGE_LOGIN, + submitted: true, + generatedEmail: email, + clicked: Boolean(clickResult.clicked), + }; +} + +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 phoneCheck = verifyHostedPhoneBeforeSubmit(values.phone); + const clickResult = await clickHostedSubmitButton({ + stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT, + label: 'hosted-paypal-card-submit', + maxAttempts: 4, + }); + return { + stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT, + submitted: true, + payloadPhone: values.phone, + ...phoneCheck, + }; +} + +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: getHostedStepKey(PAYPAL_HOSTED_STAGE_REVIEW), + 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(); + const expectedStage = String(payload.expectedStage || '').trim(); + if (expectedStage && stage !== expectedStage) { + return { + stage, + expectedStage, + submitted: false, + skipped: true, + approveReady: Boolean(findApproveButton()), + }; + } + if (stage === PAYPAL_HOSTED_STAGE_LOGIN) { + return submitHostedLogin(payload); + } + if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) { + return fillHostedGuestCheckout(payload); + } + if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) { + return clickHostedCreateAccount(payload); + } + if (stage === PAYPAL_HOSTED_STAGE_REVIEW) { + return clickHostedReviewConsent(); + } + return { + stage, + submitted: false, + approveReady: Boolean(findApproveButton()), + }; +} + +function inspectPayPalHostedState() { + const stage = detectPayPalHostedStage(); + const createAccountButton = findHostedCreateAccountButton(); + return { + url: location.href, + readyState: document.readyState, + hostedStage: stage, + hasGuestCardFields: Boolean(document.getElementById('cardNumber')), + hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()), + createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)), + 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..d29360d 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,164 @@ 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); + const buttonTextBeforeClick = getActionText(button) || '订阅'; + log(`Plus Checkout:准备点击“${buttonTextBeforeClick}”提交 OpenAI Checkout。`); + simulateClick(button); + await sleep(300); + const buttonTextAfterClick = getActionText(button); + if (buttonTextAfterClick && SUBSCRIBE_PROCESSING_TEXT_PATTERN.test(buttonTextAfterClick)) { + log(`Plus Checkout:已点击“${buttonTextBeforeClick}”,按钮进入“${buttonTextAfterClick}”,正在等待 PayPal 跳转。`); + } else { + log(`Plus Checkout:已点击“${buttonTextBeforeClick}”,正在等待 PayPal 跳转。`); + } + await sleep(900); + return { + clicked: true, + buttonText: getActionText(button), + buttonTextBeforeClick, + buttonTextAfterClick, + hostedVerificationVisible: hasHostedOpenAiVerificationDialog(), + }; +} + +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 +596,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 +800,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 +816,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 +871,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, }; @@ -1323,6 +1535,42 @@ function isBusySubscribeButton(button) { || /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) { if (!button) return null; if (button.form) return button.form; @@ -1509,12 +1757,23 @@ async function clickPlusSubscribe(payload = {}) { const subscribeButton = await waitUntil(() => { const button = findSubscribeButton(); - return button && isEnabledControl(button) && !isBusySubscribeButton(button) ? button : null; + return button || null; }, { label: '订阅按钮', intervalMs: 250, 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 performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'submit', label: 'click-subscribe' }, async () => { @@ -1522,6 +1781,8 @@ async function clickPlusSubscribe(payload = {}) { }); return { clicked: true, + subscribeButtonStatus: 'clicked', + subscribeButtonText: buttonState.text, }; } @@ -1538,6 +1799,7 @@ async function readChatGptSessionAccessToken() { async function inspectPlusCheckoutState(options = {}) { const structuredAddress = getStructuredAddressFields(); + const subscribeButtonState = getSubscribeButtonState(findSubscribeButton()); const state = { url: location.href, readyState: document.readyState, @@ -1549,7 +1811,14 @@ async function inspectPlusCheckoutState(options = {}) { paymentTextPreview: getPaymentTextPreview(), cardFieldsVisible: hasCreditCardFields(), billingFieldsVisible: hasBillingAddressFields(), - hasSubscribeButton: Boolean(findSubscribeButton()), + hasSubscribeButton: subscribeButtonState.found, + subscribeButtonBusy: subscribeButtonState.busy, + subscribeButtonEnabled: subscribeButtonState.enabled, + subscribeButtonStatus: subscribeButtonState.status, + subscribeButtonText: subscribeButtonState.text, + hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(), + hostedVerificationVisible: hasHostedOpenAiVerificationDialog(), + hostedPayPalButtonFound: Boolean(findHostedPayPalButton()), checkoutAmountSummary: getCheckoutAmountSummary(), addressFieldValues: { address1: structuredAddress.address1?.value || '', diff --git a/content/utils.js b/content/utils.js index f650814..2c439d1 100644 --- a/content/utils.js +++ b/content/utils.js @@ -505,6 +505,7 @@ function simulateClick(el) { : { method: 'click' }; let method = strategy.method || 'click'; + const textBeforeClick = el.textContent || ''; if (method === 'requestSubmit' && form && typeof form.requestSubmit === 'function') { form.requestSubmit(el); @@ -516,8 +517,8 @@ function simulateClick(el) { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } - console.log(LOG_PREFIX, `已点击(${method}): ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`); - log(`已点击(${method}) [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`); + console.log(LOG_PREFIX, `已点击(${method}): ${el.tagName} ${textBeforeClick.slice(0, 30)}`); + log(`已点击(${method}) [${el.tagName}] "${textBeforeClick.trim().slice(0, 30) || ''}"`); } /** diff --git a/data/step-definitions.js b/data/step-definitions.js index 27b0a0a..b6ea992 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,13 @@ { 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), + { 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-card', title: '无卡直绑填写 PayPal 资料', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-card' }, + { id: 9, order: 90, key: 'paypal-hosted-create-account', title: '无卡直绑确认创建 PayPal', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-create-account' }, + { id: 10, order: 100, key: 'paypal-hosted-review', title: '无卡直绑完成 PayPal 授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-hosted-review' }, + ]; const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, @@ -163,6 +171,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 +238,23 @@ ); const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); + const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_EMAIL); + const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps( + PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, + 11, + 110, + SIGNUP_METHOD_EMAIL, + { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION } + ); + const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps( + PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, + 11, + 110, + SIGNUP_METHOD_EMAIL, + { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION } + ); + const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_PHONE); + const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 11, 110, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true }); const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps( PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, @@ -313,6 +385,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 +427,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 +522,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 +733,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/flow-registry.js b/shared/flow-registry.js index a9a46ad..3f3123d 100644 --- a/shared/flow-registry.js +++ b/shared/flow-registry.js @@ -191,11 +191,22 @@ }, 'content/plus-checkout': { sourceId: 'plus-checkout', - commands: ['plus-checkout-create', 'plus-checkout-billing', 'plus-checkout-return'], + commands: [ + 'plus-checkout-create', + 'paypal-hosted-openai-checkout', + 'plus-checkout-billing', + 'plus-checkout-return', + ], }, 'content/paypal-flow': { sourceId: 'paypal-flow', - commands: ['paypal-approve'], + commands: [ + 'paypal-approve', + 'paypal-hosted-email', + 'paypal-hosted-card', + 'paypal-hosted-create-account', + 'paypal-hosted-review', + ], }, 'content/gopay-flow': { sourceId: 'gopay-flow', diff --git a/shared/settings-schema.js b/shared/settings-schema.js index 2f39824..f200001 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,11 +320,29 @@ ?? 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( - nested?.flows?.openai?.autoRun?.stepExecutionRange - ?? stepExecutionRangeByFlow.openai + stepExecutionRangeByFlow.openai + ?? nested?.flows?.openai?.autoRun?.stepExecutionRange ?? {}, defaults.flows.openai.autoRun.stepExecutionRange ), @@ -349,8 +370,8 @@ }, autoRun: { stepExecutionRange: normalizeStepExecutionRangeEntry( - nested?.flows?.kiro?.autoRun?.stepExecutionRange - ?? stepExecutionRangeByFlow.kiro + stepExecutionRangeByFlow.kiro + ?? nested?.flows?.kiro?.autoRun?.stepExecutionRange ?? {}, defaults.flows.kiro.autoRun.stepExecutionRange ), @@ -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..3d93895 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -327,6 +327,7 @@ Plus 支付